When a handler sends a url key, Django LiveView automatically records the current page state before applying the update. Pressing the browser back or forward button replays the inverse (or direct) process, restoring the DOM exactly as it was left.
No configuration is required. The history module activates as soon as the first navigation happens.
The URL must also work on a full page load (a Django view rendering the same page): it is what the browser loads after a reload, when a link is shared, or when a history entry has no snapshot.
How It Works
Every time send() includes a url key:
The current state of every DOM region that has ever been touched is saved (snapshot).
A new history entry is pushed with an internal index so
popstatecan identify it.The new HTML is applied to the target element.
When the user presses Back or Forward:
The framework saves the state of the entry being left and reads the snapshot of the target entry from
sessionStorage.All touched regions are restored to their recorded state, including the values typed in form fields.
The page title,
<html lang>attribute, and scroll position are also restored.The inline scripts of the restored regions run again (see below).
The
liveview:history-restoredevent is dispatched ondocument.
Inline Scripts
Restoring a region runs its inline <script> tags again, the same way they ran when the HTML arrived: once each, with el and this bound to the region, after calling el.__cleanup(). Scripts of a region nested in another restored region run only once, bound to the innermost one. A page keeps working after Back or Forward without extra code:
State outside the region set by its scripts (body classes, the active menu link...) matches the restored page.
Event listeners added by its scripts are attached again to the restored elements.
The restored HTML is the HTML as it was left, after the scripts modified it. Scripts are therefore expected to be safe to run again on their own output. Most are: highlighting code, rendering diagrams (Mermaid skips the ones already rendered), toggling classes, binding listeners to the elements of the region. Two things to watch:
A script that adds elements to the region adds them again. Check before adding, or keep the elements in the template.
A listener added to
documentorwindowis added again on every render and every restore. Remove it inel.__cleanup, or use a flag.
For a script that must only run when the content arrives, never on Back/Forward, add data-liveview-replay="false":
<script data-liveview-replay="false">
showConfetti();
</script>
Live Widgets: ~data-liveview-permanent~
Every region a handler updates is part of the history, even if it has nothing to do with navigation. A live widget (a visitor counter, a chat, a notifications area) would be rolled back to an old value when going back. Mark it as permanent:
<section id="notifications" data-liveview-permanent></section>
The history never snapshots nor restores a permanent element (or anything inside it), so it always shows its live state. Its scripts are not run again either. If a permanent element with an id lives inside a restored region (for example, a player present on every page), the live node is kept in place of its old copy.
Form Fields
Text inputs, checkboxes, radio buttons, selects and textareas are restored with the values the user left: a search box keeps its query, a half-written comment keeps its text. Password and file fields are never stored.
Storage
Snapshots are stored in sessionStorage (per browser tab):
โ Snapshots survive a page reload
โ Each tab has its own independent history
โ Snapshots are cleared automatically when the tab closes
A maximum of 50 entries are kept. When the limit is exceeded, the oldest snapshots are pruned. If the storage quota is exceeded, the module falls back to memory-only: snapshots are lost on reload, but back/forward still work within the same tab session.
The ~liveview:history-restored~ Event
After every restore, the framework dispatches a custom event on document:
document.addEventListener('liveview:history-restored', (event) => {
const { url, index } = event.detail;
console.log('Restored to', url, 'at index', index);
// Re-initialize any component that needs it after restore
initMyComponent();
});
Use this to re-initialize third-party libraries or components that are not handled by the inline scripts of the restored regions.
Edge Cases
Multi-step jumps: pressing Back three times or calling
history.go(-3)is handled correctly.Zigzag navigation: Back, then Forward, then Back again restores each entry as it was left.
History branching: navigating after going back discards the forward entries, exactly like native browser history.
Unknown entries: if a snapshot is missing (pruned, or older than a cleared
sessionStorage), the framework falls back to a full page reload so the server renders the correct URL.Removed elements: when
remove: Trueis sent, the parent container is snapshotted instead, so the element reappears when going back.Reload: after a reload, the current page comes from the server and the other entries keep their snapshots.
Infinite scroll: elements with
data-liveview-intersect-*are observed again after a restore. If the restored sentinel is in the viewport, it loads the next page.data-liveview-init: it is not called again on restore.
Example
@liveview_handler("navigate_to_profile")
def navigate_to_profile(consumer, content):
user_id = content["data"]["user_id"]
user = User.objects.get(id=user_id)
html = render_to_string("profile_page.html", {"user": user})
send(consumer, {
"target": "#main-content",
"html": html,
"url": f"/profile/{user.username}/",
"title": f"{user.name} - Profile"
})
After navigating to the profile, the user can press Back to return to the previous page. The previous state of #main-content is restored without a server round-trip.