Reader
Prose fragmented into pages by CSS columns, turned by scroll snap, dotted by ::column. A paginated reader that is one element and no JavaScript.
::columnCSS carousels
How it works
columns-1plus a fixedh-80makes the prose fragment: content that doesn’t fit the height goes into another column beside it, andoverflow-x-automakes those columns scrollable. One column box is one page.[column-fill:auto]fills each page top to bottom instead of balancing the last one short.::columnis the page. It’s the one handle CSS has ever had on a generated column box, and it accepts scroll-snap properties only —scroll-snap-align: centerandscroll-snap-stop: alwaysturn scrolling into page turns; nothing that affects layout applies to it.scroll-marker-group: afterplus a marker on::columngives every page a dot the browser generates, orders, and keeps in sync;:target-currentmarks the page you’re on. Clicking a dot scrolls to that page.- Keep
column-gapat least as large as the container’s side padding —px-8withgap-x-12here — or the next page peeks past the edge, since centered snapping splits the container’s inline padding evenly into the two gutters. hyphens-autoneeds thelangattribute to pick a dictionary;break-after-avoidon the heading keeps a title off the bottom of a page.- Prev/next buttons are the same
::scroll-button()recipe as Carousel; the dots already reach every page, so this demo leaves them out.
Why the CSS block
::column and ::scroll-marker are generated — there’s no element to put a class on —
and the marker needs content, which has no utility syntax. Carousel
makes the same exception for its dots and arrows. The block is optional: skip it and you
keep a scrollable block of columns, just without snap points or page dots.
Accessibility
- Pagination is layout only. The text stays in one continuous DOM order, so a screen reader reads the whole piece and nothing is hidden or duplicated.
tabindex="0"plus the focus ring makes the article keyboard-focusable. Some engines focus scrollable regions on their own; the attribute makes it explicit everywhere. Arrow keys turn pages, and mandatory snapping lands each press on a page boundary.- The generated dots form a focusgroup: the whole row is one tab stop, and arrow keys move between dots and scroll the reader.
- The heading names the region through
aria-labelledby="rdr-title". motion-safe:scroll-smooth, not plainscroll-smooth— the global reduced-motion rule only clamps transition and animation duration, and a dot click would otherwise slide a full page past someone who asked for less motion.- Size the box in
rem, not a viewport unit, so a larger font size produces more pages instead of clipped text.
The fallback story
Without ::column the generated columns can’t be targeted, so there are no snap points
and no dots. The article is still a horizontally scrollable block of columns with an
ordinary scrollbar — that’s why the scrollbar is hidden only behind
supports-[selector(::column)]:[scrollbar-width:none], so engines without dots keep
their one way to tell there’s more to scroll. Without scroll-marker-group you lose the
dots and keep the scrolling and snapping.
Browser support
Minimum stable version per engine, resolved at build time from MDN's browser-compat-data; Baseline status from the official web-features dataset. Everything degrades gracefully — the “when missing” column is the actual behavior, not a broken page.
| Feature | Chrome | Edge | Firefox | Safari | When missing |
|---|---|---|---|---|---|
| ::column pseudo-element | 135 | 135 | ✕ | ✕ | The generated columns cannot be targeted, so they get no snap points and no scroll markers. The multicol container still fragments its content into columns and still scrolls to reach them. |
| CSS carousels (::scroll-marker / ::scroll-button) | 135 | 135 | ✕ | ✕ | The scroller still scrolls and snaps; dots and arrow buttons simply do not render. |