Split-Flap Board
Departure-board characters that flip mechanically into place.
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/split-flap.jsonUsage
import { SplitFlap, SplitFlapBoard } from "@/components/ui/split-flap"
<SplitFlap value="TOKYO" length={8} />
<SplitFlapBoard
columns={[{ label: "Time", charset: "numeric" }, { label: "Destination" }, { label: "Remarks" }]}
rows={[{ cells: ["22:45", "TOKYO HND", "ON TIME"] }]}
/>Examples
Word ticker
A fixed length keeps the cell count steady while words change. The letters-only drum spins less than the full one.
Counter
A digits-only charset makes each digit step to its neighbour, like a mechanical tally.
Type to flip
Every change spins each cell forward through the drum. Characters that aren't on it drop straight in with one flip.
API reference
SplitFlap
A row of flaps. Renders a <span>; the visible cells are hidden from screen readers and the value is read once.
| Prop | Type | Default |
|---|---|---|
value | stringThe text to show. Lowercase letters are shown in uppercase when the drum has no lowercase. | None |
length | numberFixed number of cells. Short values are padded with blank flaps, long ones are cut. | value length |
align | "left"Which side short values hug when padded. | "left" |
charset | "alphanumeric"The drum order. Flaps only travel forward, so the order decides how far a change spins. Start custom drums with a space for blank. | "alphanumeric" |
flipDuration | numberMilliseconds for one flap to fall. The last flap of a run takes longer and bounces. | 90 |
stagger | numberMilliseconds between neighbouring cells starting. | 30 |
delay | numberMilliseconds before the first cell starts. | 0 |
flipOnMount | booleanStart blank and flip in once the display scrolls into view. | false |
size | "sm"Cell height (28, 40 or 60px). Every dimension follows the --flap-h variable, so a class like sm:[--flap-h:48px] also works. | inherited or 40px |
color | NeonColorPalette name or hex. Falls back to --neon-accent, then cyan. | inherited |
SplitFlapBoard
A housing of SplitFlap rows with column captions. Renders role="table" with rows and cells, so it reads as a table.
| Prop | Type | Default |
|---|---|---|
rows | { cells: string[]; colors?: (NeonColor | undefined)[] }[]One entry per row; colors apply per column index. | None |
columns | { label?, length?, align?, charset?, className? }[]Captions, cell counts, alignment and drum per column. Length defaults to the longest value. className applies to the column's cells and caption, e.g. max-sm:hidden. | None |
header | ReactNodeTitle bar above the board, e.g. a label and a clock. | None |
charset | stringDefault drum for columns without their own. | "alphanumeric" |
flipDuration | numberMilliseconds per flap. | 90 |
stagger | numberMilliseconds between cells. Updates ripple diagonally from the top-left. | 24 |
flipOnMount | booleanStart blank and flip every row in once the board is in view. | false |
size | "sm"Cell height. Override with a --flap-h class for responsive boards. | "md" |
color | NeonColorColor for every cell without its own. | inherited |
Accessibility
- Each SplitFlap reads its value once from visually hidden text; the flapping cells are aria-hidden, so nothing is announced character by character.
- Add aria-live="polite" to a SplitFlap when its changes should be announced, as the ticker example does.
- SplitFlapBoard is a role="table" with column headers, so screen reader users can move cell by cell. Name it with aria-label.
- The flaps are driven by one shared requestAnimationFrame loop that writes transforms directly and stops when every cell has settled.
- With reduced motion, cells switch to the new character without flipping.