Quick Start
1. Add to your HTML <head>:
Code snippet omitted: it ships with the download.
The one-line inline script sets a JS flag so controls that need JavaScript can hide themselves when it is unavailable. Keep data-cfasync="false" if you are behind Cloudflare Rocket Loader.
2. Add the board anywhere in your <body>:
Code snippet omitted: it ships with the download.
3. Add before the closing </body> tag:
Code snippet omitted: it ships with the download.
Write the real text inside each [data-flap-text] element. The script reads it, replaces it with generated flap cells, and keeps the true text in a hidden mirror for screen readers, so the plain text is what ships in your HTML. This example matches the included demo: it resolves on load and points an external shuffle button at the board's id; switch data-trigger to scroll when the board begins below the fold.
Options
Set these on the container that carries data-flap.
| Attribute | Values | Default | Description |
|---|---|---|---|
data-flap |
(present) | n/a | Marks the board. Required. |
data-charset |
Any string | ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.:% |
Characters a cell rolls through on its way to its destination. Uppercased automatically. |
data-speed |
Any number | 1 |
Roll speed multiplier. 2 is twice as fast, 0.5 half. |
data-stagger |
Seconds | 0.045 |
Delay between neighbouring cells. Rows lag by four times this, which is what makes the wave. 0 resolves the whole board at once. |
data-rolls |
Whole number | 8 |
How many characters a cell passes through before landing. Below 3 the roll reads as a single flip; above about 14 the board takes too long to settle. |
data-trigger |
scroll, load |
scroll |
scroll resolves the board when it comes into view (once). load resolves it shortly after the page is ready. |
data-glint |
true, false |
true |
The light sweep across the board as the last cell lands. |
Per-row attributes, on each [data-flap-text] element:
| Attribute | Values | Default | Description |
|---|---|---|---|
data-flap-text |
(present) | n/a | Marks text to be turned into flap cells. Required on every row. |
data-flap-alts |
Strings split by | |
none | Alternative destinations for this row. Each shuffle picks one at random (never the current one). Without alts, a shuffle re-rolls the row back to the same text. |
data-charset |
Any string | inherits the board's | Overrides the roll characters for this row. Numeric rows look much better rolling digits only. |
Markup hooks:
| Attribute | Goes on | Description |
|---|---|---|
data-flap-rows |
The wrapper around the rows | Gets aria-live="polite" so a resolve is announced once. Optional; the board itself is used if absent. |
data-flap-shuffle |
A <button> |
Re-flips every cell to a new destination. Put it inside the board, or anywhere on the page with data-flap-shuffle="board-id" pointing at the board's id. |
Performance
Every cell renders four glyph layers inside its own 3D context, so keep a board to roughly 40 cells in total (the demo board uses 30 across five rows). Past that, mid-range phones start dropping frames during the roll; the script logs a console warning if you go over. If you need more, split the content into several boards with their own scroll triggers so only the one on screen is ever rolling.
Each cell's roll is one timeline with one tween per flap, not per frame, and both channels (the hinge rotation and the shadow) are driven from that single tween through gsap.quickSetter.
Accessibility
- Screen readers never hear the roll. The generated cells are inside an
aria-hidden="true"wrapper, and the true text lives in a visually hidden mirror inside the same element, so the row reads as "312", never as a stream of rolling characters. The rows wrapper is markedaria-live="polite"and the mirrors are all rewritten once, when the board finishes resolving. - Reduced motion: no cells are generated at all. Every row stays as the plain styled text that shipped in your HTML, and the shuffle control hides itself, since there is nothing for it to flip.
- JavaScript off, or a blocked CDN: identical to the reduced-motion state. Nothing is hidden by CSS that only JavaScript can un-hide.
- Keyboard: the shuffle control is a real
<button>, so Enter and Space work, and it carries a visible:focus-visiblering. - Touch: the board is time-driven rather than pointer-driven, so it behaves the same on a phone, with a slightly shorter roll to keep frames cheap. The shuffle button is a 48px-minimum touch target and goes full width on small screens.
Dependencies
Required:
- GSAP 3.15.0
- ScrollTrigger plugin (only used by
data-trigger="scroll")
No other libraries and no images. The demo sets the cells in Mona Sans with font-variant-numeric: tabular-nums; the cell box is a fixed --cell-w / --cell-h, so any face works, including a mono one if you prefer that register.
Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.