A Solari-style split-flap display for React. CSS 3D transforms, no canvas, no WebGL, no video. Copy-paste friendly.
Built by Cody Shanley. MIT licensed.
Pick whichever fits your project.
npx shadcn@latest add https://codyshanley.com/r/split-flap.jsonThis drops three files into your project:
components/split-flap-slat.tsxcomponents/split-flap-display.tsxstyles/split-flap.css
Import the CSS once in your root layout / global stylesheet:
import "@/styles/split-flap.css";Grab the three files from registry/split-flap/ and drop them into your project wherever you keep components. Import the CSS once in your app.
Not published. The component is ~400 lines total. Copy-paste is the point — you own the code, tweak it freely, upgrades are diffs you can read.
import { SplitFlapSlat, SPLITFLAP_ALPHABET } from "@/components/split-flap-slat";
import { useEffect, useState } from "react";
function Cycle() {
const [target, setTarget] = useState("A");
useEffect(() => {
const id = setInterval(() => {
setTarget((t) => {
const i = SPLITFLAP_ALPHABET.indexOf(t);
return SPLITFLAP_ALPHABET[(i + 1) % SPLITFLAP_ALPHABET.length];
});
}, 900);
return () => clearInterval(id);
}, []);
return <SplitFlapSlat target={target} size="xl" />;
}import { SplitFlapDisplay } from "@/components/split-flap-display";
<SplitFlapDisplay
words={["DEPARTURES", "ARRIVALS", "BOARDING", "DELAYED", "ON TIME"]}
anchorWord="DEPARTURES"
slotCount={10}
holdMs={2600}
size="default"
/>const [tick, setTick] = useState(0);
<SplitFlapDisplay
words={["FLIP", "BUZZ", "ZOOM", "NEON", "JAZZ"]}
slotCount={4}
autoRotate={false}
refreshSignal={tick}
/>
<button onClick={() => setTick(t => t + 1)}>Next</button>| Prop | Type | Default | What it does |
|---|---|---|---|
words |
string[] |
required | Pool of words to cycle through. |
anchorWord |
string |
— | First word to land on. If set, later picks use a shuffled-deck cycle. |
slotCount |
number |
8 |
How many letter cells. Padded with spaces if the word is shorter. |
holdMs |
number |
3000 |
How long each word sits before the next one flips in. |
stepMs |
number |
110 |
Milliseconds per individual letter flip. |
slotStaggerMs |
number |
40 |
Delay between each slat starting its flip, left to right. |
size |
"compact" | "hero" | "default" | "xl" |
"default" |
Preset sizing. See below. |
autoRotate |
boolean |
true |
Set false to drive transitions from outside via refreshSignal. |
refreshSignal |
number |
— | Incrementing this moves to the next word immediately. |
| Prop | Type | Default | What it does |
|---|---|---|---|
target |
string |
required | The character this slat should flip to. |
stepMs |
number |
110 |
Milliseconds per single-letter flip. |
startDelayMs |
number |
0 |
Delay before the first flip starts. |
minFlips |
number |
1 |
Force at least N flips even if target is the next letter. |
onFlip |
() => void |
— | Fired on each flip tick. |
size |
"compact" | "hero" | "default" | "xl" |
"default" |
Preset sizing. |
frozenMidFlip |
boolean |
false |
Freeze at −45° for inspection. |
staticLetter |
string |
— | Force a letter without animation (debug). |
initialCurrent |
string |
" " |
What the slat shows on mount before flipping. |
| Preset | Slat size |
|---|---|
compact |
28 × 42 px |
hero |
28 × 40 px |
default |
42 × 62 px |
xl |
120 × 180 px |
Every slat reads these CSS variables, so you can override them per-slat or globally:
| Variable | Default | What it controls |
|---|---|---|
--splitflap-font |
system-ui, sans-serif |
Font stack used for letters. |
--slat-ink |
#1A1A19 |
Letter color. |
--slat-top-grad-a / --slat-top-grad-b |
creamy whites | Top-half panel gradient. |
--slat-bottom-grad-a / --slat-bottom-grad-b |
creamy whites | Bottom-half panel gradient. |
--slat-width / --slat-height |
42px / 62px |
Per-slat dimensions (overridden by size preset classes). |
Dark-mode example:
.dark .splitflap-slat {
--slat-ink: #F3EFE6;
--slat-top-grad-a: rgba(30, 30, 30, 0.9);
--slat-top-grad-b: rgba(20, 20, 20, 0.9);
--slat-bottom-grad-a: rgba(22, 22, 22, 0.9);
--slat-bottom-grad-b: rgba(28, 28, 28, 0.9);
}Wrap the display in .splitflap-nocard to strip the card gradients and shadows, leaving only the letters flipping in place. Useful for inline-with-text hero lines.
<span className="splitflap-nocard">
<SplitFlapDisplay ... />
</span>Each letter is its own SplitFlapSlat. A slat has a static back half (visible at rest) and two animated halves that fold down and up in sequence over ~110ms, split into a 50ms fold and a 60ms unfold. To go from A to R, the slat walks forward through the alphabet one letter at a time, matching the way physical Solari boards work — each flap is attached to the next on a chain. The supported alphabet is A-Z 0-9 . space (forward-only, wraps around).
For a longer writeup with the inspiration, history, and interactive demos, see the case study on codyshanley.com.
- Letters are wrapped in
aria-hidden="true"so screen readers don't read individual flap states. - The display itself is a
role="status"region witharia-live="polite"and a visually-hidden copy of the current word, so assistive tech hears the whole word once it lands. - Respects
prefers-reduced-motion: reduce— the flip animation is disabled and slats snap directly to their target letter.
Issues and PRs welcome. This is a single-purpose component — the goal is to keep it small, readable, and copy-paste friendly, not to grow a library. Bug reports and accessibility fixes are especially appreciated.
MIT — see LICENSE.
Built by Cody Shanley · Portfolio · GitHub
If you use this, I'd love to see where it ended up — send a link.
