How these pages navigate. The rail on the left swaps the content beside it instead of reloading the document — the theme’s first HTMX surface, and the pattern to copy when a second one earns it.
a fixed rail, rebuilt at the top
The rail is a long position: fixed column with its own
scrollbar. Every full-page navigation rebuilt it from scratch — click
Toggle deep in the Fields group and the new page’s rail arrived
scrolled to the top, having forgotten where you were.
The usual fixes restore state: stash the scroll offset, replay it on load. This shell does the other thing — it stops destroying the state. A navigation that never touches the rail has nothing to restore.
hx-boost · hx-select · hx-push-url
The rail’s links carry htmx attributes; the pages they point at are the
static build’s ordinary HTML. hx-select carves the content
column out of the full response, so the build is the API —
no endpoint was added, no fragment is served, and the same URL works with
the script, without it, and in a search result.
<div id="railNav" class="docs-rail"
hx-boost="true" hx-target="#docsMain" hx-select="#docsMain"
hx-select-oob="#railNav" hx-ext="head-support"
hx-swap="outerHTML show:window:top" hx-push-url="true"> Boost stays on the rail, deliberately. Content links may point anywhere — another section, an example site, off-site — and swapping an arbitrary page into a docs shell would be worse than a reload. The rail only ever points at pages that share this shell, which is what makes the swap safe to promise.
server truth over client re-derivation
A partial swap leaves everything outside the fragment one page stale. Each of those loose ends is owned by the mechanism that cannot get it wrong, rather than by a script that re-derives it:
hx-select-oob="#railNav"
takes the response’s own rail, active state already rendered, and
swaps it in alongside the content. The scroll still survives, because
scrolling belongs to #docsRail, the container, and the
out-of-band swap replaces only its child. The swapped-in rail re-arms its
own boost; htmx processes what it inserts.
hx-select discards the response’s
<head>, so the head-support extension merges it
back on every hop. (Use the htmx-ext-head-support package: the
copy inside htmx.org/dist/ext is the htmx 1 build and warns
under 2.)
.docs-shell:has(.sidenav-end): the stylesheet asks the DOM
whether a Jump-to rail is actually present. Derived, not declared —
there is nothing left to go stale.
island-docs-nav.js — the residue
The island keeps only what no swap can know. On any arrival — deep link, search hit, a plain navigation — it centres the rail’s active link, because there was no swap to preserve anything. And on a phone, where the rail is a collapse over the content, it closes the menu once a choice is made. Everything else it once handled is listed in its header comment with a forwarding address.
when to reach for this, and when to stop
This pattern earns its place when three things line up: the swapped region is expensive state (a scrolled rail), every target shares the shell, and the fallback is the same link working normally. The fields section of the CMS docs is exactly that shape; most of the site is not, which is why nothing else is boosted.
And a tripwire for later: every loose end above was one piece of page state that full reloads kept consistent for free. Three moved to declarative homes; one stayed as script. The day a surface needs a growing list of after-swap handlers is the day to stop patching and morph the whole document instead — or to admit the reload was fine.