Skip to content

codemanshan/splitflap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

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

Auto-rotating word cycle

import { SplitFlapDisplay } from "@/components/split-flap-display";

<SplitFlapDisplay
  words={["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);

<SplitFlapDisplay
  words={["FLIP", "BUZZ", "ZOOM", "NEON", "JAZZ"]}
  slotCount={4}
  autoRotate={false}
  refreshSignal={tick}
/>
<button onClick={() => setTick(t => t + 1)}>Next</button>

Props

<SplitFlapDisplay>

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.

<SplitFlapSlat>

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.

Sizes

Preset Slat size
compact 28 × 42 px
hero 28 × 40 px
default 42 × 62 px
xl 120 × 180 px

Theming

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);
}

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.

<span className="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

No releases published

Packages

 
 
 

Contributors