Chapter 15

The long page

Ten minutes of reading is a small journey: whoever sets out wants to know where they are, how much is left, and to find their place again if they wander off. These are the details that keep company without getting in front of the text.

12 min read

The last chapter was about components you touch once and that end. This one changes the scale of time: not a one-second gesture, but a ten-minute read. The question becomes different — not “how do I answer a click?”, but “how do you keep company with someone reading for ten minutes?”. A long page is a small journey, and travellers have simple needs: to know where they are, how much is left, to find their place again if they wander off, to read an aside without losing the thread.

These are humble needs, and for that reason easy to overlook. Almost all of the playground chases micro-interactions that last a heartbeat; here the details must last, accompany without tiring, stay useful at the first minute as at the ninth. It’s a particular kind of honesty: the risk isn’t lying about a state, it’s faking progress, celebrating a milestone the reader hasn’t reached yet, stealing the scroll from them “to help”.

Five details, five reading companions. And as always, no libraries: the browser already knows scroll-spy with the IntersectionObserver, scroll timelines in CSS, the Popover API, anchoring, localStorage. Our job is only to orchestrate them with measure — so the protagonist stays the text, and the details know how to stand one step back.

The index that says “you are here”

The first companion is a map. On a long page the reader easily loses the sense of where they are: a sticky index gives it back, and updates itself as you scroll.

15.a

The opening

A long page begins with a promise: it’s worth staying. The index alongside isn’t there to impress, it’s there to reassure — it tells you how many stops there are and which one you’re on right now.

The highlighted entry isn’t "the one at the top of the screen". It’s the one you’re actually reading: the section sitting on the reading line, a third below the edge.

The rhythm

Scroll slowly and the index follows slowly. It doesn’t anticipate, it doesn’t fidget: the highlight moves when your eye is genuinely on the new section, not a moment before.

An IntersectionObserver does the work, not a scroll listener measuring everything every frame. The observer lives on the compositor; the main thread stays free for reading.

The measure

An honest index fits in one column without scrolling. When the entries grow too many, the sign isn’t "enlarge the index": it’s "split the article". The shape of the index measures the shape of the text.

Here the sections are five, and five is already a good number for a ten-minute read: enough stops to find your way, not so many they become noise.

The keyboard

Each entry is a real link, with visible focus. With Tab you move through them in order; with Enter you jump to the section. The index lets itself be used by mouse and keyboard without either feeling like an afterthought.

The active entry carries aria-current="location": a screen reader announces "current location" as you scroll. Colour is the second signal, not the first.

The farewell

Once you reach the end, the index has done its job. It kept you company without ever getting in front of the text. That’s the best way to measure whether a component is honest: you only think of it when you look for it.

There’s no celebration for getting here. The end of a long read is a private gesture: the index simply marks the last stop and falls silent.

Sticky index · the current section highlights itself

Scroll the article on the right and watch the index on the left: the active entry changes on its own. But choosing “what’s active” isn’t trivial — it isn’t the topmost section, it’s the one you’re actually reading. An IntersectionObserver with a rootMargin that raises the reading line to 35% from the edge decides which section holds your gaze, and the highlight moves when your eye is already there, not a moment before. It’s scroll-spy done on the compositor, not a listener re-measuring everything every frame.

The index is a real <nav> with a list of in-page links, and the active entry carries aria-current=“location”: a screen reader announces “current location”, colour is the second signal, not the first. A click scrolls to the section smoothly — but under prefers-reduced-motion it becomes a clean jump, because smooth-scroll is motion in every sense. And when the viewport narrows the index disappears: on a small screen the space goes to the content, not to a contents column stealing room.

The bar that says “how much is left”

Second companion: progress. A thin bar at the top answers the most frequent question of a long read — how much remains? — and the best answer is carved into the CSS, not computed by hand.

15.b

The page compositor

A thin reading bar, fixed at the top, is a silent pact: I tell you where you are and how much is left, asking nothing in return. It isn’t a loader, it’s a position map.

Its fill is driven by the scroll of this window, not by time. Stop and it stops; scroll back and it goes back. That’s the difference between an honest indicator and a promise of progress.

The secret is where the math runs. With animation-timeline: scroll() the bar lives on the compositor: the browser paints it without disturbing the main thread, and it never skips a beat even as the eye races.

The number on the right, by contrast, is updated by JavaScript, because CSS animates the width and not the text. It uses tabular figures: going from 9 to 10 the line doesn’t jolt. Precision isn’t vanity, it’s calm.

Where animation-timeline doesn’t exist yet, a small script with a passive scroll event pushes the same bar. Same result, different path — and the native version, when present, always wins.

You’re almost at the bottom. The last stretch of a long read is the one earned with the patience of the first two thirds: the bar doesn’t celebrate it, it just records it.

Reading bar · animation-timeline: scroll() with fallback

The bar’s fill is tied to the container’s scroll with animation-timeline: scroll(): a scroll timeline in pure CSS, zero JavaScript for the movement. It runs on the compositor, and that changes everything — it doesn’t jolt, doesn’t skip a beat while the eye races, doesn’t take up the main thread. An @supports (animation-timeline: scroll()) opens the native path; where it’s still missing, a small script with a passive scroll event pushes the same bar. Same result, two roads, and the native one always wins.

The percentage number, by contrast, is updated by JavaScript, because CSS animates the width and not the text — and it uses tabular figures, so going from 9 to 10 the line doesn’t jolt. Under prefers-reduced-motion there’s nothing to remove: this isn’t a time-based animation, it’s a position mapping. Reducing motion doesn’t mean hiding information, and the bar stays tied to the scroll exactly as before. It’s the distinction chapter 02 kept repeating: you remove the path, never the state.

The note that doesn’t move you

Third companion: the aside. The footnote is a great editorial invention, but on screen it has an old flaw — the jump. You click, land at the bottom, lose your place. An anchored popover solves it at the root.

15.c

A note that doesn’t move you

The footnote is a great editorial invention, but on screen it has a flaw: the jump. Press the number and you land at the bottom, far from where you were reading.

An anchored popover keeps the note right there, beside the reference. Read the aside, press Esc, and the paragraph never moved. It’s the same old idea, dressed in a gentler interaction.

Note 1

The reference is a <sup> wrapping a <button>: the keyboard reaches it with Tab, a screen reader announces it as a button. The tiny graphic takes nothing from the semantics.

Note 2

Popover API and CSS Anchor Positioning do the heavy lifting: top layer, light dismiss, Esc and anchoring to the number. No library, no hand-rolled focus trap.

Footnote · Popover API + CSS Anchor Positioning

The little reference number opens a note right where you’re reading, anchored to the reference itself. Under the skin two native APIs do the heavy lifting: the Popover API (popovertarget + popover=“auto”) handles top layer, light-dismiss and Esc without a line of code; CSS Anchor Positioning glues the popover to the number with position-anchor and flips itself toward free space with position-try-fallbacks. No hand-rolled focus trap, no tooltip library.

The reference is a <sup> wrapping a real <button>: the keyboard reaches it with Tab, a screen reader announces it as a button, and the popover has role=“note”. The extra care we add by hand is focus restore: when the note closes — with the X, with Esc, or by clicking outside — focus returns to the reference that opened it, so the keyboard isn’t left orphaned at the top of the page. It’s the difference between a component that works and one that lets itself be used with eyes closed.

The button that appears only when needed

Fourth companion: the return shortcut. At the top of the page a “back to top” is useless — you’re already at the top. That’s why it mustn’t be there at the start: it should appear only when returning to the top has become a journey.

15.d

Scroll down, then look bottom-right

Scroll down. For now, bottom-right, there’s nothing: you’re still near the top, and a "back to top" would just be a button stating the obvious.

Keep going. The threshold is 35% of the page: that’s where returning to the top stops being a one-flick gesture and becomes a journey. Only then does the button earn its place.

There it is: now that you’re far enough down, it has appeared with a brief entrance. It doesn’t flash, it doesn’t bounce — it rises into place and stops, as befits a tool, not a notification.

When you climb back above the threshold it leaves, and faster than it arrived: things exit the stage quicker than they enter. It’s the same physical coherence as an object getting out of the way.

Press it and the page rises with a smooth scroll — a clean jump, instead, if you asked for less motion. Either way you end up where you wanted: at the top.

This is the last paragraph, far enough down to make the button genuinely useful. Try it: the only honest way to show a "back to top" is to give you a page worth returning from.

Back to top · appears after a scroll threshold

Scroll down inside the window: at first, bottom-right, there’s nothing. The button appears only after 35% of scroll, the threshold past which climbing back stops being a flick and becomes a trek. Appearing earlier would be noise — a button stating the obvious; appearing later is a service. And the appearance respects the law of chapter 01: it rises into place in about 220ms, and when you climb back above the threshold it leaves faster than it arrived, because things exit the stage quicker than they enter.

It’s a real <button> with an accessible label, and while invisible it’s pulled from the flow with hidden: so you don’t meet it with Tab when it isn’t needed, and it returns to the tab order only once visible. The click brings you back to the top with a smooth scroll — a clean jump, again, under prefers-reduced-motion — and then moves focus to the content, not to a button that’s already vanishing. All inside a fake scrollable container, so the demo doesn’t move the real page: an honest “back to top” is one that genuinely has a page worth returning from.

The bookmark that doesn’t fake progress

The last companion is the most delicate for honesty. Someone reading for ten minutes sometimes leaves and comes back: a bookmark gives the spot back. But it must give back a position, not pass off progress.

15.e

The ribbon between the pages

A paper bookmark doesn’t cheat: it doesn’t tell you you’ve understood half the book, it just tells you which page you were on. This does the same. It saves the position, not a score.

Scroll a little and press "Place the bookmark here". From then on the browser remembers the exact height you were at — not continuously, but when you decide or when you leave the page.

On reopening it doesn’t jump back on its own: that would feel arrogant. It offers to resume, and nothing more. Returning to the spot is your choice, not an automatism that snatches the scroll from you.

The difference from a progress bar is subtle but decisive: the bar says "how much is left", the bookmark says "where you stopped". The first is a measure, the second is a memory.

If the browser denies memory — private mode, full storage, permissions — the component says so plainly instead of pretending. A bookmark that lies would be worse than none.

You’re at the bottom. Drop the bookmark here, reload the page in your mind: you’ll find it waiting, exactly where you left it. Not a pixel ahead, not one behind.

Scroll a little, then drop the bookmark where you are.

Bookmark · saves to localStorage, honest about where you stopped

Scroll a little, press “Place the bookmark here”: the browser remembers in localStorage the exact height you were at — not continuously, but when you decide or when you leave the page. On reopening it doesn’t jump back on its own: that would feel arrogant, it snatches the scroll from you. It offers to resume, with a banner saying “you stopped here, about N%”, and leaves you the choice between returning and starting over. Restoring is a smooth scroll, a clean jump under reduced motion.

Here honesty is all in the difference between position and progress. A bar says “how much is left”; a bookmark says “where you stopped” — the first is a measure, the second is a memory. That’s why it never writes “67% complete” as if you’d understood that 67%: it shows where you were, not a progress grade. It’s a ribbon between the pages, not a report card. And when the browser denies memory — private mode, full storage, denied permissions — the component declares it plainly instead of pretending: a bookmark that lies would be worse than no bookmark.


Five companions, one discipline: accompany without getting in front. The index that says “you are here”, the bar that says “how much is left”, the note that doesn’t move you, the button that appears only when needed, the bookmark that remembers where you were without faking how much you understood. None of them is the protagonist — the protagonist is the text, and ten minutes of attention that deserve respect. Keeping company with someone reading at length isn’t filling the margins with gadgets: it’s removing every small friction of “where am I, how much is left, how do I get back” so silently the reader doesn’t notice, and can stay elsewhere in their mind, inside the words.

The next chapter stays on the long page but shifts its gaze: from accompanying the reader to composing the space they move through. The editorial grid — what makes a page elegant before it’s even beautiful?