Chapter 05

The scroll that tells a story

Scrolling is the most transparent gesture we have. It asks no permission, it interrupts nothing, it needs no teaching: we already know how to do it. A page that goes along with it — instead of chasing it — is a page that lets itself be read to the end.

9 min read

There’s a gesture, in the way we use the web, that we never had to learn: scrolling. There is no tutorial for the scroll, no voice that says “you can keep reading if you go down”. And yet it’s the most common gesture we make in a day in front of a screen — the thumb going up, the wheel turning, the spacebar pressed in the middle of a paragraph. A page worth its salt is a page that recognises this gesture as the user’s main action, and that decides what to do with it.

Scrolling is a narrative variable: it can be the time of a story, the axis of an argument, the cadence of a reveal. But it can also be the noise of an interface that chases the thumb — revealing, hiding, animating, twitching. The difference isn’t how much the page “moves”, it’s what moves and when. The pages that tell something with the scroll do it with a light hand: a progress map, a detail entering, a title that stays. Never an orchestra.

In this chapter we open six different ways to “scroll well”. From the most discreet — a bar that only says where you are — to the most structured — an illustration that changes as the story moves forward. Six ways, one philosophy: scrolling is the user’s gesture, and the interface that respects it is the one that lets itself be scrolled without running after you.

The bar that knows where you are

In any reasonably long read there’s a moment when the eye looks for a handhold: how much is left? The best answer isn’t a huge percentage, and it isn’t “scroll to the end to find out”. It’s a thin little bar, at the top, that fills as you scroll — nothing more. A position map, not an animation.

05.a

Una piccola finestra

Le interfacce che chiedono concentrazione devono restituire fiducia. Una barra di lettura sottile, ferma in alto, è un patto silenzioso: ti dico dove sei e quanto manca, senza distrarti.

Il movimento di questa barra è guidato dallo scroll della finestra qui sotto, non dal tempo: se ti fermi, lei si ferma; se torni indietro, lei torna indietro. È un indicatore di posizione, non un'animazione.

Il valore percentuale a destra usa cifre tabulari — così quando passa da 9 a 10 non sussulta. La precisione tipografica non è vanità: è la condizione per cui un numero "vivo" non diventa fastidio.

Cosa la barra non fa

Non lampeggia. Non si gonfia all'arrivo. Non celebra il 100% con un fuoco d'artificio. Arrivare in fondo a una lettura è un gesto privato: la barra si conclude e basta.

Su prefers-reduced-motion: reduce non cambia nulla — non c'è motion qui, c'è solo mappatura. La differenza è importante: ridurre il movimento non significa togliere informazione.

Il fallback gentile

Nei browser che non supportano animation-timeline: scroll() la barra resta a zero. Un piccolo script di sicurezza la spinge col scroll event passivo, così il valore arriva comunque — ma la versione nativa, quando c'è, è di gran lunga preferita: gira sul compositor, mai sul main thread.

Continua a scorrere. Quasi finito.

L'ultimo paragrafo è il momento più delicato di una lettura lunga: chi è arrivato qui ha deciso di starci. Tutto quello che si guadagna in fondo, si guadagna con la pazienza dei primi due terzi.

Reading bar · scroll-driven, no theatre

The bar is driven by the scroller’s scroll via animation-timeline: scroll(self) — CSS reads the position straight from the compositor, without going through the main thread. No scroll event, no requestAnimationFrame: zero JS for rendering. On browsers without support, a passive-listener fallback gives the same result — more traditional, more expensive, but honest.

The percentage value on the right uses font-variant-numeric: tabular-nums: fixed-width digits, so when it goes from 9 to 10 it doesn’t twitch. It’s an invisible detail until you remove it — exactly the kind of care that separates “a bar that works” from “a bar that’s pleasant to watch”. Reduced motion changes nothing, because there is no motion here: there’s only a mapping. An important distinction that returns in the other demos.

Elements that reveal themselves once

The “fade-up on scroll” pattern is one of the most overused on the web. When it works, the page seems to wake up as you go down; when it’s applied without judgement, it seems to chase the thumb. The rule is simple: only once. If you scroll back up and down, the elements stay put. Pages that re-animate on every pass feel restless; pages that animate once feel lived in.

05.b

Sei carte sotto. Ognuna entra una volta sola, alla sua prima apparizione in viewport. Se torni indietro, restano ferme.

  • 01

    Il primo gesto

    Una scheda nuova entra dal basso, salendo di pochi pixel. L'occhio nota il movimento, non lo subisce.

  • 02

    Il ritardo onesto

    Ogni scheda ritarda la sua entrata di 40 ms rispetto alla precedente. Stagger, ma corto: niente coda di pavone.

  • 03

    Una sola volta

    Se scrollo sopra e sotto, queste schede non rifanno la scena. La memoria visiva si costruisce, non si interrompe.

  • 04

    La soglia

    Il trigger scatta a 18% dell'altezza visibile. Più basso sarebbe nervoso, più alto farebbe "saltare" gli ultimi.

  • 05

    Reduced motion

    Chi ha chiesto meno movimento riceve subito tutto al posto giusto. L'informazione non si conquista: appare.

  • 06

    Lo spazio è già lì

    L'opacità parte da 0, ma lo spazio della scheda è già riservato. Nessun salto del layout, mai.

Reveal on first entry · no chasing

An IntersectionObserver with an 18% threshold and unobserve after the first trigger. Each card gets a permanent data-revealed=“true” attribute; CSS transitions run from there. The stagger between cards is 40 ms — short, no peacock tail. Longer would be celebration, shorter would be one thing entering all at once.

The card’s space is already reserved in layout: opacity starts at 0 but geometry doesn’t. It’s the difference between “reveal” and “layout shift”. On prefers-reduced-motion: reduce all cards appear immediately in their final place and the observer isn’t even set up — whoever asked for less motion doesn’t get “less motion”, they get the information. An important point: reducing motion doesn’t mean slowing down, it means delivering the same final state without the journey.

The title that stays, and changes in silence

When an article is divided into sections, each with its own title, there’s a subtle moment: the reader gets halfway through a section and forgets where they are. The best pages help without being asked — a small sticky element up top, with the current section’s title, that changes with a gentle cross-fade when a new one enters. Not a menu, not a breadcrumb: just a typographic label.

05.c

Il primo respiro

Una sezione esiste perché l'occhio sa dove sta. Quando il titolo lavora per chi legge, la lettura cambia ritmo da sola — non serve dire "torna su per ricordartelo".

Lo sticky della testata fa un solo lavoro: dirti, nella periferia della vista, dove sei. Non fa ombra, non si gonfia, non chiede attenzione. È un dettaglio tipografico, non un componente.

Il cross-fade del titolo dura 260 ms. Basta. Più lungo si trasformerebbe in un'animazione celebrativa; più corto sembrerebbe un salto. La distanza fra "movimento" e "rumore" è di poche decine di millisecondi.

Continuiamo. La prossima sezione arriva da sola.

La pausa

Una sezione esiste perché l'occhio sa dove sta. Quando il titolo lavora per chi legge, la lettura cambia ritmo da sola — non serve dire "torna su per ricordartelo".

Lo sticky della testata fa un solo lavoro: dirti, nella periferia della vista, dove sei. Non fa ombra, non si gonfia, non chiede attenzione. È un dettaglio tipografico, non un componente.

Il cross-fade del titolo dura 260 ms. Basta. Più lungo si trasformerebbe in un'animazione celebrativa; più corto sembrerebbe un salto. La distanza fra "movimento" e "rumore" è di poche decine di millisecondi.

Continuiamo. La prossima sezione arriva da sola.

La ripresa

Una sezione esiste perché l'occhio sa dove sta. Quando il titolo lavora per chi legge, la lettura cambia ritmo da sola — non serve dire "torna su per ricordartelo".

Lo sticky della testata fa un solo lavoro: dirti, nella periferia della vista, dove sei. Non fa ombra, non si gonfia, non chiede attenzione. È un dettaglio tipografico, non un componente.

Il cross-fade del titolo dura 260 ms. Basta. Più lungo si trasformerebbe in un'animazione celebrativa; più corto sembrerebbe un salto. La distanza fra "movimento" e "rumore" è di poche decine di millisecondi.

Continuiamo. La prossima sezione arriva da sola.

Il commiato

Una sezione esiste perché l'occhio sa dove sta. Quando il titolo lavora per chi legge, la lettura cambia ritmo da sola — non serve dire "torna su per ricordartelo".

Lo sticky della testata fa un solo lavoro: dirti, nella periferia della vista, dove sei. Non fa ombra, non si gonfia, non chiede attenzione. È un dettaglio tipografico, non un componente.

Il cross-fade del titolo dura 260 ms. Basta. Più lungo si trasformerebbe in un'animazione celebrativa; più corto sembrerebbe un salto. La distanza fra "movimento" e "rumore" è di poche decine di millisecondi.

Continuiamo. La prossima sezione arriva da sola.

Sticky heading · cross-fade between sections

One sticky element, not one per section. When a new section crosses the “reading line” (IntersectionObserver rootMargin -15% 0px -55% 0px), the text inside the slot cross-fades — old rises and fades out, new rises and enters. Duration 260 ms. Longer would turn into a celebration animation; shorter would feel like a jump.

The pattern avoids a classic stumble: the “double sticky” that overlaps itself. Having one title per section that becomes sticky in turn creates moments where two titles are visible at the same time, one leaving and one entering. Keeping one typographic slot, and rotating its content, is simpler and more honest. It also works better with screen readers: the element is marked aria-hidden=“true” because it’s a visual duplicate of the real titles below.

Depth that doesn’t make you sick

Parallax is an old trick — Disney cartoons used it long before browsers did. On the web it’s lived through a decade of abuse: layers chasing each other, backgrounds galloping, scenes “revealing” themselves like at a fairground. The honest version is almost invisible: layers move by a handful of pixels, proportions are coherent (the moon slower than the ridges, the ridges slower than the pebbles), and on prefers-reduced-motion everything stops. Atmosphere, not spectacle.

05.d
Capitolo zero

Il fondale si muove appena

Tre layer dietro a queste parole. Scorri e nota: la luna in cima si sposta di pochi pixel, le creste a metà altezza un po' di più, i sassolini in basso quasi quanto il testo.

La parallasse onesta non si vede a colpo d'occhio. Si vede quando la togli: la pagina sembra piatta, mentre prima sembrava semplicemente "buona". Quello scarto sottile è il regalo del trucco.

La regola dei millimetri vale anche qui: meno di 20 px di translazione su tutto il viaggio dello scroll. Oltre, la stomaco se ne accorge prima dell'occhio.

Le tre quote di profondità sono coerenti: più un layer è "lontano", più lentamente si muove. È la fisica: la luna è più stabile delle nuvole, le nuvole sono più stabili degli alberi.

Hai chiesto meno motion? Tutta la parallasse è spenta — niente trucchetti morbidi, niente versione "soft". Il rispetto è binario.

Honest parallax · under 20px across the whole scroll

Three sticky layers at the top of the scroller, animated with animation-timeline: scroll() on the single Y axis. Amplitudes: −18px on the “far” layer (moon and stars), −8px on mid (ridges), −3px on near (pebbles). Nothing more. A parallax you can see at a glance is one parallax too many: the eye appreciates it, the stomach doesn’t.

Respecting physics — the farther a layer is, the slower it moves — isn’t fetishism: it’s why depth feels real instead of “effect-like”. On prefers-reduced-motion: reduce we don’t offer a “soft” version with reduced amplitudes: we turn everything off. Respecting the setting is binary; whoever asked for less motion doesn’t want a watered-down trick, they want no trick at all.

Two threads, no scrub

Scrollytelling — text on the left scrolling, image on the right changing — is one of the web’s most beautiful narrative forms, and one of its most fragile. The “millimetric” version (every scroll pixel moves an illustration pixel) feels magical for ten seconds and then becomes nervous: on low bandwidth, with keyboard, with a screen reader, it stops meaning anything. The stepped version is more honest: three discrete states, three clear thresholds, a short transition between them.

05.e

Un seme, un albero, un bosco

Tre passi, tre stati. Scorri per cambiarli.

01

Il seme

Tutto comincia in piccolo. Una proprietà CSS, un evento, una transizione di 200 ms. Nessuna di queste cose, da sola, fa una buona interfaccia.

Ma è da queste briciole che la qualità si compone: la cura del singolo dettaglio, ripetuta cento volte, è quello che il lettore "sente" senza poterlo nominare.

02

L'albero

I componenti si combinano in pattern. Una conferma diventa un'azione, un'azione una rotta, una rotta una sezione. La pagina prende forma — diventa abitabile.

A questo livello la decisione più importante non è "cosa fa", è "come si comporta quando l'utente sbaglia". L'errore gentile è la firma del progettista.

03

Il bosco

Quando i pattern si stratificano, nasce una voce. Non è scelta a tavolino: emerge dalle ripetizioni piccole. Le interfacce che hanno una voce sono quelle che chi le usa riconosce a colpo d'occhio.

Il bosco non è la somma degli alberi. È quello che resta quando i singoli alberi spariscono — il ritmo, il silenzio, l'aria fra una cosa e l'altra.

(Fine. Torna su per ricominciare.)

Stepped scrollytelling · seed, tree, forest

Text scrolls normally; the side panel is in position: sticky and changes state when a new text section crosses the viewport’s central zone (IntersectionObserver with rootMargin -20% 0px -20% 0px). The three illustrations — seed, tree, forest — live in the same SVG, overlapped; the active one becomes opaque with a small scale-up, the others fade. Cross-fade, not morph.

Why stepped and not continuous scrub? Because on keyboard, screen reader, and 200% zoom, continuous scrub is essentially invisible: there’s no semantic “halfway” between “seed” and “tree”. Stepped, each state is a unit of meaning, reachable and announceable. On mobile (below 32rem) the side panel becomes a card on top — same markup, different geometry, semantics intact.

The index that follows, never stands in front

A long article divided into sections wants an index — not a menu, not a breadcrumb: a small column that says “you’re reading this”. The active entry moves on its own as you scroll, and clicking it takes you there with a soft scroll. The rule of discretion: the index exists as long as it serves. On a narrow screen it disappears — it doesn’t turn into a hamburger, it doesn’t ask for attention: it gives the space back to the text, which is the real protagonist.

05.f

Premessa

Un indice è onesto solo se sa stare al suo posto. Troppo grande, distrae; troppo piccolo, non serve. La giusta misura è una colonna sottile, sempre visibile, mai prepotente.

La voce attiva qui non è "quella più in alto". È quella che stai davvero leggendo — la sezione con la maggior parte di sé sopra la riga della lettura.

Il metodo

Un IntersectionObserver guarda ogni sezione e segnala quando la sua intersezione col viewport supera una soglia. Il rootMargin alza la linea di "lettura" al 30% dal bordo superiore — così l'evidenziazione cambia quando l'occhio è davvero sulla nuova sezione, non un attimo prima.

Niente listener di scroll, niente getBoundingClientRect ad ogni frame. L'osservatore lavora sul compositor, e il main thread resta libero per quello che conta.

Le voci

Il marker laterale si sposta con una transizione corta — 220 ms — perché l'occhio non perda il filo del cambio. Più lungo sarebbe nostalgico, più corto sarebbe brusco.

Tab funziona come da specifica: ogni voce è un link onesto, con focus visibile. Il marker tiene conto anche del focus da tastiera: se "punto" l'ultima voce con Tab, lui mi segue. È un indice che si lascia usare in due modi.

I limiti

Su viewport stretti l'indice scompare. Niente "hamburger toc": è un dettaglio editoriale, non navigazione critica. Su schermo piccolo lo spazio va al contenuto.

Su un articolo molto lungo, oltre 12 voci, l'indice diventa rumore. La regola: se non sta in una colonna senza scroll, è il segno che l'articolo va diviso in capitoli — non l'indice da ingrandire.

Il congedo

L'indice che hai usato per orientarti svanisce quando l'articolo finisce. Niente sticky che resta a fluttuare nel vuoto. Il dettaglio si toglie quando non serve più.

Questo è il modo migliore per misurare se un componente è onesto: se ci pensi solo quando lo cerchi.

Floating ToC · scroll-spy with side marker

Five sections, five entries in the index. The active entry is chosen by an IntersectionObserver measuring the largest visible intersection, with rootMargin: ‘-30% 0px -50% 0px’ to lift the “reading line” — the highlight changes when the eye is actually on the new section, not when its edge has just entered. The side marker (the accent on the left border) slides with a 220 ms transition, enough not to lose the thread, little enough not to feel like a trick.

Links work with the keyboard: visible focus, scroll-behavior: smooth for the navigation, scroll-margin-block-start on sections so the title doesn’t end up glued to the edge. On prefers-reduced-motion smooth scroll becomes instant and the marker transition disappears: whoever asked for less motion gets the same information, in the same place, without the journey. Same principle as the first demo: reducing motion doesn’t mean hiding state.

What we take away

Scrolling is the most transparent gesture we have. A page that goes along with it — instead of chasing it — is a page that lets itself be read to the end. The six demos above show six ways to “tell with the scroll” without taking themselves too seriously:

  1. The reading bar that maps position, scroll-driven, never animated.
  2. The reveal that happens once, because a page that re-animates on every pass is a restless page.
  3. The sticky title that changes in silence — one slot to avoid double-stickies.
  4. The honest parallax, under 20 px, with a complete off-switch on reduced motion.
  5. The stepped scrollytelling — three discrete states, semantics intact on keyboard.
  6. The floating index that highlights, never stands in front, and gives space to the reader.

Six ways, one rule: scrolling is the user’s gesture, and the interface that respects it is the one that lets itself be scrolled without running after you. The next chapter changes register — numbers that have weight — because after teaching the scroll to tell a story, it’s time to let the quantities speak too: tabular figures, honest count-ups, signed deltas, and formats that recognise who’s looking.