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";functionCycle(){const[target,setTarget]=useState("A");useEffect(()=>{constid=setInterval(()=>{setTarget((t)=>{consti=SPLITFLAP_ALPHABET.indexOf(t);returnSPLITFLAP_ALPHABET[(i+1)%SPLITFLAP_ALPHABET.length];});},900);return()=>clearInterval(id);},[]);return<SplitFlapSlattarget={target}size="xl"/>;}import{SplitFlapDisplay}from"@/components/split-flap-display";<SplitFlapDisplaywords={["DEPARTURES","ARRIVALS","BOARDING","DELAYED","ON TIME"]}anchorWord="DEPARTURES"slotCount={10}holdMs={2600}size="default"/>const[tick,setTick]=useState(0);<SplitFlapDisplaywords={["FLIP","BUZZ","ZOOM","NEON","JAZZ"]}slotCount={4}autoRotate={false}refreshSignal={tick}/><buttononClick={()=>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.
<spanclassName="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.
