Split Flap Display HTML CSS JS — Solari Board

Split Flap Display · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

CSS 3D transforms: perspective plus rotateX hinges recreate physical flap rotation
Dual transform-origin: top flap hinges at bottom center, bottom flap at top center for two-beat flips
backface-visibility hidden: keeps the reverse of each rotating leaf from showing through
Step-through cycling: cells flap one character at a time through CHARSET toward the target
Double requestAnimationFrame: reliably triggers CSS transitions from JavaScript
Staggered timing: per-column setTimeout offsets create a left-to-right ripple
Character clipping: overflow hidden and translateY split one glyph across the seam
Auto-cycling presets: setInterval rolls through messages like a departures board
Custom input: type any eight-character message and commit with Set or Enter
Pure DOM and CSS: no images, sprites, canvas or external libraries

About this UI Snippet

How to Build a Split-Flap Display with CSS 3D Transforms and JavaScript

Screenshot of the Split Flap Display snippet rendered live

A split-flap display — also called a Solari board — is the mechanical-looking sign you see in old train stations and airports, where each character cell physically flips through letters until it lands on the target. This recreation uses CSS 3D transforms, the perspective and rotateX properties, and a JavaScript animation engine that cycles each cell through the character set one flap at a time. No images, sprites or libraries are involved — just the DOM and CSS.

The anatomy of one flap cell

Each character is a .flap-cell with fixed dimensions and, crucially, a CSS perspective value that creates the 3D viewing frustum so rotations look like they recede into the screen. Inside the cell are five layered elements: a static .top half showing the upper portion of the current character, a static .bot half showing the lower portion, a thin .divider line across the middle to mimic the seam of a physical flap, and two animated flaps — .flap-top-anim and .flap-bot-anim — that perform the actual flip and are hidden when idle.

Both halves clip their character. The top half uses overflow: hidden with the glyph aligned to the bottom, and the bottom half aligns the glyph to the top using transform: translateY(-50%) on its inner <span>. The result is that a single full-height character appears split exactly across the horizontal seam — the top half shows the upper part, the bottom half shows the lower part, exactly like a real flap.

How the flip animation works

A physical split-flap works in two beats: the top leaf bearing the old character rotates downward and out of view, then the bottom leaf bearing the new character rotates up into place. The code reproduces this precisely with rotateX and two transform-origin settings.

The top animated flap has transform-origin: bottom center, so it hinges along the seam. It starts at rotateX(0deg) showing the previous character, then transitions to rotateX(90deg), folding away from the viewer until it is edge-on and invisible. backface-visibility: hidden keeps its reverse side from showing. The bottom animated flap has transform-origin: top center and starts folded at rotateX(-90deg) showing the next character edge-on, then transitions to rotateX(0deg) to snap flat into the final position. Sequencing the two with short setTimeout delays (about 55ms each) produces the convincing click-down motion.

A subtle but essential trick: before animating, the code sets transition: none, applies the start transform, and then in a double requestAnimationFrame re-enables the transition and applies the end transform. The double rAF guarantees the browser has committed the start state to the layout before the transition begins, otherwise the browser would collapse both writes into one and no animation would play. This is the standard pattern for triggering CSS transitions from JavaScript reliably.

The character set and cycling logic

A constant CHARSET string defines the available glyphs in order: a leading space, then A–Z, then 0–9. Each cell stores a current index and a target index into this string. The animation does not jump straight to the target — like a real board, it steps one character forward at a time. animateStep advances current by one using modular arithmetic (current + 1) % CHARSET.length, performs one flip animation, and when finished schedules itself again with a short delay. It stops only when current === target. So changing a cell from A to G visibly flaps through B, C, D, E, F, G — the signature behavior of these boards.

Staggering columns and message handling

displayMessage pads or trims the input to exactly eight characters with padEnd and slice, then for each cell looks up the target character's index in CHARSET (falling back to space for unknown characters). To avoid a flat, simultaneous flip, each cell's animation start is delayed by i * 40 milliseconds via setTimeout, so the columns ripple from left to right — exactly how mechanical boards updated row by row. A per-cell animating flag prevents overlapping animation chains if a new message arrives mid-flip.

Building cells and wiring the UI

On load, the script creates eight cells with createElement, injects the inner markup with innerHTML, and stores each cell's state object in a cells array. Preset buttons are generated from a PRESETS array, each calling displayMessage on click. A text input lets users type custom messages (uppercased via CSS text-transform and JS), committing on the Set button or Enter key. Finally a setInterval auto-cycles through the presets every few seconds so the board is always alive, mirroring a departures board rolling through its schedule.

The whole effect — flipping leaves, the mechanical seam, the left-to-right ripple, the step-through-the-alphabet motion — comes entirely from layered absolutely-positioned divs, two rotateX hinges with opposite transform-origin, and a small recursive timing engine. It is a compact masterclass in CSS 3D and JavaScript-driven transitions.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to trace every hinge rotation and timing offset by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to walk through exactly why the two animated flaps need opposite transform-origin values, or why a single requestAnimationFrame isn't enough to reliably trigger the transition. The same assistant can help optimize it — checking whether the recursive animateStep calls could be replaced with a single driving loop for boards with many more cells, or whether the per-step setTimeout chain introduces visible jitter under load. It's just as useful for extending the effect: ask it to add a sound on each flap, wire the board up to a live data feed like a real countdown or ticket queue, or support a taller multi-row board for longer messages. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an airport-style "split-flap display" (Solari board) in plain HTML, CSS, and JavaScript using only CSS 3D transforms — no canvas, no video, no sprite images, no libraries.

Requirements:
- A row of fixed-size cells, one per character, each with CSS perspective set on the cell so child rotations look like real 3D hinges rather than a flat squash.
- Each cell is built from five layered pieces: a static top half and static bottom half showing the resting character (clipped with overflow hidden, top half aligned to its top edge, bottom half aligned to its bottom edge, so together they show one full-height glyph split across a seam), a thin divider line across the middle, and two animated flap elements that are hidden except during a flip.
- The top animated flap must have transform-origin: bottom center and rotate from rotateX(0deg) to rotateX(90deg) to fold away and reveal the character underneath. The bottom animated flap must have the opposite hinge, transform-origin: top center, and rotate from rotateX(-90deg) up to rotateX(0deg) to snap the new character into place. Both need backface-visibility: hidden so their reverse side never flashes into view mid-rotation.
- Every cell tracks a current index and a target index into a shared character set (a leading space, then A-Z, then 0-9). A recursive step function must advance the current index by exactly one position per call (wrapping with modular arithmetic), play one two-beat flip animation for that single step, and call itself again until current equals target.
- Before triggering each rotation, apply the start transform with transition: none, then re-enable the transition and apply the end transform inside two nested requestAnimationFrame calls (not just one).
- When a new message is set, stagger each column's animation start by an increasing delay (e.g. columnIndex * 40ms) so the whole board updates as a left-to-right ripple.
- Add a text input and a Set button to commit a custom message, plus a row of preset buttons and a setInterval that auto-cycles through presets every few seconds.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Watch it cycleOn load the board auto-cycles through preset messages, flapping each cell through the alphabet to its target.
  2. 2
    Click a presetPick one of the preset buttons to flip the board to that message instantly.
  3. 3
    Type custom textEnter up to eight characters in the input field — letters, numbers or spaces.
  4. 4
    Set the messagePress the Set button or hit Enter to animate the board toward your custom text.
  5. 5
    Observe the rippleNotice how columns start their flip staggered left to right for a mechanical board feel.
  6. 6
    Adapt the charsetEdit the CHARSET constant in the JS to add symbols or change the available glyphs.

Real-world uses

Common Use Cases

Retro hero headers
Animate a headline or tagline on a landing page with nostalgic mechanical charm, alongside effects like text particles.
Live status boards
Display departures, scores or queue numbers that flip when values change.
Countdown and reveals
Build anticipation for a launch or event with characters flapping into place.
Brand microsites
Add a memorable Solari-board signature to a product or campaign page.
Teaching CSS 3D
A focused example of perspective, rotateX and transform-origin next to a color wheel picker.
Game scoreboards
Show scores, lives or level names with playful flip animations.

Got questions?

Frequently Asked Questions

It mimics a real split-flap board, which can only step one leaf forward at a time. The code advances the current index by one per animation and repeats until it reaches the target, so it visibly rolls through intermediate characters.

To trigger a CSS transition you must apply the start state, let the browser commit it, then apply the end state. Two nested rAF calls guarantee the start transform is painted before the transition begins, otherwise the browser batches both writes and skips the animation.

Both halves use overflow hidden. The top aligns the glyph to its bottom edge and the bottom shifts its glyph up with translateY(-50%), so together they show the upper and lower portions of the same full-height character across the seam.

Yes. Add the characters to the CHARSET constant. The cycling logic uses CHARSET.indexOf and modular arithmetic, so it automatically handles any length, though longer sets mean more flips to reach distant characters.

The static top and bottom halves display the resting character. The animated flaps are only shown during a flip to perform the rotation, then hidden again so the cell renders crisply without extra layered elements.

Yes. Click JSX for a React component, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for a utility-class version. In React, drive each character cell from props and trigger the flip animation in useEffect when the target text changes.