Binding UI Initialization to Turbo’s turbo:load Event
Learn why DOMContentLoaded fails with Turbo Drive and how to reliably initialize UI widgets using the turbo:load event, complete with a worked example and cache‑aware tips.
07 Oct 2025, 11:41 UTC

The problem: widgets that never re‑initialize after a Turbo navigation
When you add a date picker, a chart library, or any UI widget that attaches itself during DOMContentLoaded, the widget works on the first page load but disappears after you click a link that Turbo Drive intercepts. Turbo Drive fetches the new HTML, replaces <body>, and does not create a new document, so DOMContentLoaded never fires again. The widget’s initialization code runs once and is then lost, leaving the enhanced element as a plain HTML node.
Why turbo:load is the reliable hook
Turbo emits a custom event named turbo:load after every successful page replacement, including the initial load. Listening to this event guarantees that your setup code runs each time Turbo presents a new body. To avoid double‑initialization when Turbo restores a cached preview, you can either guard the setup with a flag or tear down the previous instance before creating a new one.
Basic pattern
document.addEventListener('turbo:load', function () {
// teardown if needed (optional)
if (window.myDatepicker) {
window.myDatepicker.destroy();
window.myDatepicker = null;
}
// fresh initialization
const inputs = document.querySelectorAll('input[type="date"]');
inputs.forEach(function (el) {
window.myDatepicker = new Datepicker(el, { format: 'yyyy-mm-dd' });
});
});
The teardown block is optional for widgets that safely tolerate re‑initialization, but many charting libraries require you to destroy the old instance first.
Worked example: initializing a Flatpickr date picker
Suppose your server‑rendered template contains:
<input type="text" class="date-picker" data-behavior="flatpickr">
Add the following script to your layout (it can sit in the head or just before the closing body tag):
document.addEventListener('turbo:load', function () {
document.querySelectorAll('[data-behavior="flatpickr"]').forEach(function (el) {
if (el._flatpickr) {
el._flatpickr.destroy();
}
flatpickr(el, { dateFormat: 'Y-m-d' });
});
});
To verify that the handler runs on every Turbo navigation:
- Open Chrome DevTools → Settings → Preserve log.
- Navigate to a page with the date picker, open the console, and note the first log from
turbo:load. - Click a same‑origin link that Turbo intercepts (e.g., a navigation link).
- Observe that a new
turbo:loadlog appears and the date picker is functional again.
If you instead relied on DOMContentLoaded, you would see the log only once and the picker would be inactive after the first Turbo navigation.
Trade‑off: Turbo’s preview cache can resurrect stale state
Turbo keeps a snapshot of the last rendered body in a preview cache. When you navigate back, Turbo may instantly restore that snapshot instead of re‑requesting the server. Any client‑side state you created after turbo:load (scroll position, opened menus, chart instances) will appear exactly as it was when the snapshot was taken, which can be confusing.
Two practical ways to handle this:
- Opt‑out elements – Add
data-turbo-permanentto any element whose client‑side state should survive a snapshot restore (e.g., a fixed header). Turbo will move the element unchanged between renders. - Force a reload – On elements where stale state is unacceptable, add
data-turbo-track="reload". Turbo will discard the preview cache and perform a full navigation for that link or form.
You can check whether a navigation used the preview cache by looking at the turbo:before-cache and turbo:cache events in the console.
Actionable closing: verify your Turbo version and adopt the pattern
Before relying on turbo:load for critical UI, confirm which Turbo version your project uses:
# Run in your project root npm ls @hotwired/turboThis command requires only read access to the node_modules directory. The output shows the installed version; if you see
@hotwired/turbo@7.xyou have the classic morphing behavior, while@hotwired/turbo@8.xenables the newer page‑refresh and morphing features. Adjust any version‑specific code accordingly.Next steps:
- Replace all
DOMContentLoadedUI initialization listeners withturbo:load.- Add a teardown step for libraries that need it.
- Audit any elements that should persist across snapshots and add
data-turbo-permanentwhere appropriate.- Test navigation flows with DevTools’ “Preserve log” enabled to ensure no stray
DOMContentLoadedlogs remain.By binding your setup to Turbo’s guaranteed
turbo:loadevent, you keep widgets alive across every Turbo‑driven page change without sacrificing the speed benefits of Hotwire.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.