djust docs
Browse documentation

Runtime Layout Switching

On this page

Change the surrounding layout (nav, sidebar, footer, wrapper markup) during a LiveView session without a full page reload. Inner state — form values, scroll position, focused elements, third-party widget instances — is fully preserved.

Phoenix 1.1 added this; djust's equivalent is self.set_layout(path).

Quick start

from djust import LiveView
from djust.decorators import event_handler

class EditorView(LiveView):
    template_name = "editor/page.html"

    def mount(self, request, **kwargs):
        self.fullscreen = False

    @event_handler
    def enter_fullscreen(self, **kwargs):
        self.fullscreen = True
        self.set_layout("layouts/fullscreen.html")

    @event_handler
    def exit_fullscreen(self, **kwargs):
        self.fullscreen = False
        self.set_layout("layouts/app.html")

Any template that renders through Django's template loader works — set_layout just queues the path. The layout is rendered with the view's get_context_data() only. No request is attached, so context-processor variables (request, user, csrf_token, messages, perms) are unavailable, and a layout that uses them renders differently than it did at page load. Pass anything the layout needs through get_context_data().

How it works

  1. self.set_layout(path) stores the path on the view (overwrites any prior pending layout — last write wins).
  2. After the event handler returns, the WebSocket consumer drains the pending path, renders the template with the view's current context, and emits a {"type": "layout", "path": ..., "html": ...} frame alongside the usual VDOM patches.
  3. The client parses the new HTML, finds the [data-djust-root] (or [dj-root]) placeholder inside it, and physically moves the live root element from the current body into the new layout. The document <body> is then replaced with the reconstructed body.

Because the live [dj-root] element is the same DOM node before and after the swap, everything attached to it survives:

  • <input> values currently being typed
  • dj-hook instances and their internal state
  • Third-party JS libraries (charts, maps, editors) that stored references to nodes inside the root

Focus and scroll position are not preserved. The root is briefly detached from the document during the move, so the browser drops focus (it returns to <body>) and resets the scroll offset of containers inside the root. The swap does not save or restore them; if you need either, restore it from a djust:layout-changed listener.

Listening for the swap

A djust:layout-changed CustomEvent fires on document after every swap:

document.addEventListener('djust:layout-changed', (e) => {
    console.log('layout is now', e.detail.path);
    // Re-initialize anything that depends on the new layout's outer markup,
    // e.g. a sidebar menu collapse handler.
});

Known limitations

  • <head> is not merged. If your new layout needs different stylesheets or scripts, add them to the original layout's <head> at mount time. The swap only replaces <body>.
  • Third-party scripts loaded in <body> are destroyed. Analytics bootstraps, Intercom widgets, customer-support chat overlays, and any other JavaScript that lives in the old <body> — including any DOM elements they created and any timers/intervals they registered against those elements — are removed when <body> is replaced. Load third-party scripts from <head> instead, or re-initialize them from a djust:layout-changed event listener. If a third-party widget is critical, consider whether this feature is the right fit for your app.
  • Outer-layout event listeners (e.g. a click handler on a <nav> that the initial layout owned) are lost — the old <body> is replaced wholesale. Put such listeners on the document / use event delegation.
  • set_layout is imperative. No declarative layout_template = "..." class attribute — that's a separate future feature.
  • One layout per render cycle. Calling set_layout twice in the same handler keeps only the last call (intentional — the client only applies one swap).

Use cases

PatternMount withOn event
Admin ↔ public togglelayouts/public.htmlself.set_layout("layouts/admin.html")
Fullscreen editorlayouts/app.htmlself.set_layout("layouts/fullscreen.html")
Onboarding → applayouts/onboarding.htmlself.set_layout("layouts/app.html") on completion
Focus mode for readinglayouts/default.htmlself.set_layout("layouts/reader.html")

Errors

  • Template not found — logged as a warning (djust.websocket), swap is skipped. The WebSocket stays alive; the view continues normally with the current layout.
  • Template rendering error — with DEBUG=True the exception is re-raised so it surfaces during development. With DEBUG=False it is logged with a traceback and the swap is skipped.
  • No [dj-root] in the incoming HTML — client refuses the swap and logs a console warning. This usually means the new layout template doesn't include a <div dj-root></div> placeholder — add one.

See also

  • LiveView lifecycle — for how get_context_data is called
  • Navigation — for routing between different LiveViews (as opposed to swapping layout within one)