Skip to content

Repository files navigation

splitflap

A Solari-style split-flap display for React. CSS 3D transforms, no canvas, no WebGL, no video. Copy-paste friendly.

→ Live demo

Split-flap panel demo

Built by Cody Shanley. MIT licensed.


Install

Pick whichever fits your project.

Option 1 — shadcn CLI (recommended)

npx shadcn@latest add https://codyshanley.com/r/split-flap.json

This drops three files into your project:

  • components/split-flap-slat.tsx
  • components/split-flap-display.tsx
  • styles/split-flap.css

Import the CSS once in your root layout / global stylesheet:

import"@/styles/split-flap.css";

Option 2 — copy-paste

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.

Option 3 — npm

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.


Usage

A single slat flipping the alphabet

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"/>;}

Auto-rotating word cycle

import{SplitFlapDisplay}from"@/components/split-flap-display";<SplitFlapDisplaywords={["DEPARTURES","ARRIVALS","BOARDING","DELAYED","ON TIME"]}anchorWord="DEPARTURES"slotCount={10}holdMs={2600}size="default"/>

Controlled advance (you decide when to flip)

const[tick,setTick]=useState(0);<SplitFlapDisplaywords={["FLIP","BUZZ","ZOOM","NEON","JAZZ"]}slotCount={4}autoRotate={false}refreshSignal={tick}/><buttononClick={()=>setTick(t=>t+1)}>Next</button>

Props

<SplitFlapDisplay>

PropTypeDefaultWhat it does
wordsstring[]requiredPool of words to cycle through.
anchorWordstringFirst word to land on. If set, later picks use a shuffled-deck cycle.
slotCountnumber8How many letter cells. Padded with spaces if the word is shorter.
holdMsnumber3000How long each word sits before the next one flips in.
stepMsnumber110Milliseconds per individual letter flip.
slotStaggerMsnumber40Delay between each slat starting its flip, left to right.
size"compact" | "hero" | "default" | "xl""default"Preset sizing. See below.
autoRotatebooleantrueSet false to drive transitions from outside via refreshSignal.
refreshSignalnumberIncrementing this moves to the next word immediately.

<SplitFlapSlat>

PropTypeDefaultWhat it does
targetstringrequiredThe character this slat should flip to.
stepMsnumber110Milliseconds per single-letter flip.
startDelayMsnumber0Delay before the first flip starts.
minFlipsnumber1Force at least N flips even if target is the next letter.
onFlip() => voidFired on each flip tick.
size"compact" | "hero" | "default" | "xl""default"Preset sizing.
frozenMidFlipbooleanfalseFreeze at −45° for inspection.
staticLetterstringForce a letter without animation (debug).
initialCurrentstring" "What the slat shows on mount before flipping.

Sizes

PresetSlat size
compact28 × 42 px
hero28 × 40 px
default42 × 62 px
xl120 × 180 px

Theming

Every slat reads these CSS variables, so you can override them per-slat or globally:

VariableDefaultWhat it controls
--splitflap-fontsystem-ui, sans-serifFont stack used for letters.
--slat-ink#1A1A19Letter color.
--slat-top-grad-a / --slat-top-grad-bcreamy whitesTop-half panel gradient.
--slat-bottom-grad-a / --slat-bottom-grad-bcreamy whitesBottom-half panel gradient.
--slat-width / --slat-height42px / 62pxPer-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);
}

No-card variant

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>

How it works

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.


Accessibility

  • Letters are wrapped in aria-hidden="true" so screen readers don't read individual flap states.
  • The display itself is a role="status" region with aria-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.

Contributing

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.


License

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.

Releases

Packages

Contributors

Languages