Aaditya

Split Flap

Component 1

About

A split flap board for status words and changing numbers. Only the characters that change flip, in fixed width cards so rows never jump. Reduced motion swaps instantly.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Split Flap to your project and fits it to your theme.

Code

How Split Flap is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client"

import * as React from "react"
import { SplitFlap } from "@/components/ui/split-flap"

/** Steps through `count` states every `ms`, for demo timelines. Stops while the tab is hidden. */
function useTicker(count: number, ms: number) {
  const [i, setI] = React.useState(0)
  React.useEffect(() => {
    const id = window.setInterval(() => {
      if (!document.hidden) setI((v) => (v + 1) % count)
    }, ms)
    return () => window.clearInterval(id)
  }, [count, ms])
  return i
}

type Row = { job: string; frames: [status: string, eta: string][] }

// Each job steps through its own timeline; the board only flips cards that change.
const rows: Row[] = [
  {
    job: "Render hero video",
    frames: [
      ["RUNNING", "00:42"],
      ["RUNNING", "00:31"],
      ["RUNNING", "00:18"],
      ["RUNNING", "00:06"],
      ["DONE", "--:--"],
      ["DONE", "--:--"],
    ],
  },
  {
    job: "Transcribe interview",
    frames: [
      ["QUEUED", "01:20"],
      ["QUEUED", "01:20"],
      ["RUNNING", "01:04"],
      ["RUNNING", "00:47"],
      ["RUNNING", "00:29"],
      ["RUNNING", "00:12"],
    ],
  },
  {
    job: "Index product docs",
    frames: [
      ["RUNNING", "00:09"],
      ["DONE", "--:--"],
      ["DONE", "--:--"],
      ["DONE", "--:--"],
      ["QUEUED", "02:10"],
      ["RUNNING", "01:52"],
    ],
  },
  {
    job: "Upscale batch 12",
    frames: [
      ["QUEUED", "03:05"],
      ["QUEUED", "03:05"],
      ["QUEUED", "02:40"],
      ["FAILED", "--:--"],
      ["RETRYING", "02:55"],
      ["RUNNING", "02:31"],
    ],
  },
]

/** A job board: status words and ETAs flip card by card, only where a character changed. */
function SplitFlapDemo() {
  const t = useTicker(6, 2200)

  return (
    <div className="grid w-full max-w-xl grid-cols-[minmax(0,1fr)_auto_auto] gap-x-6">
      <div className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-subtle) border-(color:--chatcn-border) col-span-3 grid grid-cols-subgrid border-b pb-2 font-medium">
        <span>Job</span>
        <span>Status</span>
        <span>ETA</span>
      </div>
      <ul className="divide-(color:--chatcn-border) col-span-3 grid grid-cols-subgrid divide-y">
        {rows.map((row) => {
          const [status, eta] = row.frames[t] ?? row.frames[0] ?? ["", ""]
          return (
            <li key={row.job} className="col-span-3 grid grid-cols-subgrid items-center py-3">
              <span className="text-[0.875rem]/[1.25rem] tracking-[-0.003em] text-(color:--chatcn-fg) truncate">{row.job}</span>
              <SplitFlap
                value={status}
                length={8}
                className={status === "FAILED" ? "text-(color:--chatcn-red)" : undefined}
              />
              <SplitFlap value={eta} className="text-(color:--chatcn-fg-muted)" />
            </li>
          )
        })}
      </ul>
    </div>
  )
}

/** The line under each 21st demo that points back to the kit. */
function MoreOn({ slug, children }: { slug: string; children: React.ReactNode }) {
  return (
    <p className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-faint) text-center text-balance">
      {children}{" "}
      <a
        href={`https://chatcn.dev/docs/components/${slug}`}
        target="_blank"
        rel="noopener"
        className="text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) underline underline-offset-2"
      >
        chatcn.dev
      </a>
    </p>
  )
}

export default function SplitFlapPreview() {
  return (
    <div className="flex min-h-[520px] w-full flex-col items-center justify-center gap-10 p-6">
      <SplitFlapDemo />
      <MoreOn slug="split-flap">Only the cards that change flip. More boards and loaders on</MoreOn>
    </div>
  )
}

More by Aaditya

View all
  • Dither Orb

    Dither Orb

    7
  • Pixel Orb

    Pixel Orb

    3
  • Voice Orb

    Voice Orb

    2
  • Dither Gradient

    Dither Gradient

    1
  • Wireframe Globe

    Wireframe Globe

    1
  • Particle Text

    Particle Text

    1
  • Agent Lanes

    Agent Lanes

    1
  • Audio Visualizer

    Audio Visualizer

    1

Explore more

Part of chatcn — browse the full library on 21st.dev.