Posts open in a panel, not a popup
Every article is a real page with a real URL — the sliding panel is just a faster way in.
I wanted articles to open the way they do in a good mobile app: the list stays where it is, the text slides in over it, and a swipe puts you back. What I did not want was the usual price of that trick — content that only exists inside JavaScript, URLs you can't share, and a back button that does something surprising.
The rule I settled on
The panel is a view, never a storage location. Each post is a normal page at its own address. The link on the blog page points straight at it, and the panel is layered on top of that link:
<a href="/blog/reading-panel/" data-reader>…</a>
Without JavaScript — or with a middle-click, or Ctrl+click — that's an ordinary navigation to an ordinary page. With JavaScript, a click is intercepted, the same page is fetched, its <article class="post"> is lifted out, and the address bar is updated with history.pushState. One source of truth, two ways to read it.
Why <dialog>
The panel is a native <dialog> opened with showModal(). That single call brings a lot along for free:
- Escape closes it.
- Focus is trapped inside, and the page behind becomes inert.
::backdropgives a real dimming layer with no extra element.
Animating it used to be the awkward part, because an element that flips to display: none can't transition. Modern CSS fixed that with allow-discrete and @starting-style:
.reader {
translate: 100% 0;
transition:
translate 320ms cubic-bezier(.22, .61, .36, 1),
display 320ms allow-discrete,
overlay 320ms allow-discrete;
}
.reader[open] { translate: 0 0; }
@starting-style {
.reader[open] { translate: 100% 0; }
}
The back button
Opening the panel pushes a history entry; closing it calls history.back() instead of closing directly, and the popstate handler does the actual closing. The result is that the browser's back gesture and the panel's own close button are literally the same action — there is no way for the two to disagree.
The swipe
On touch devices a horizontal drag follows your finger and closes past about 90 pixels. Vertical movement is handed back to the scroller immediately, so reading never feels like fighting the gesture, and touch-action: pan-y keeps the browser from claiming the swipe first.
If the fetch fails for any reason, the code does the least clever thing available: it navigates to the URL the link pointed at all along. That's the whole design, really — every enhancement has a plain page underneath it.