Split-flap departure-board text for the web — every letter rolls, flips or flickers into place. A <split-flap> element for HTML, Vue, Svelte and Angular, a React component and a framework-free player
| Framework | Checked with | How |
|---|---|---|
| ✅ HTML | any page, no build step | <split-flap> |
| ✅ React | 18 and 19 | <Splitflap> |
| ✅ Next.js | 16, App Router | <Splitflap>, plain text on the server |
| ✅ Vue | 3.5 | <split-flap> |
| ✅ Svelte | 5 | <split-flap> |
| ✅ Angular | 22 | <split-flap> |
| ✅ Anything else | — | SplitflapBoard on any element |
| ✅ Chrome | ✅ Firefox | ✅ Safari | ✅ Safari on iPhone |
|---|
import { Splitflap } from '@enavermate/splitflap/react';
<Splitflap text={gate} transition="flip" />;<script
type="module"
src="https://cdn.jsdelivr.net/npm/@enavermate/splitflap@0/dist/element.js"
></script>
<split-flap text="GATE A3" transition="flip"></split-flap>- 🎰 Four transitions —
flip,reel,rollandscramble, for words and for numbers - 🔤 Any text, any script — Latin, Cyrillic, Greek, Armenian, Georgian, the Indic and Southeast Asian scripts, the kana and many numeral systems roll through their own alphabets; any other character still changes, in one step. Flags, emoji and Indic syllables stay whole in one cell
- 📏 Cells that keep still — tabular digits by default, and two cell widths: text-like
naturalanduniform, a station board's fixed cells - 🎨 Styled like text — font, size, weight, colour and letter spacing come from your CSS
- 🔁 Interruptible — a new text mid-flight continues from where every cell is, without a jump
- ⏳ Loading built in — random words spin while data loads, then the real text lands
- ♿ Accessible — the text stays in the DOM for screen readers, search and copying; the canvas is
hidden from them, and
prefers-reduced-motionshows changes at once - 🖥️ Server rendering — renders as plain text on the server and in jsdom, then animates once mounted: the Next.js App Router and an app's tests need nothing extra
- 🧩 Every framework — React, a custom element for Vue, Svelte, Angular and plain HTML, and the player itself for anything else. No dependencies
npm install @enavermate/splitflap| Import | What it gives you |
|---|---|
@enavermate/splitflap/react |
<Splitflap> for React 18 and 19 |
@enavermate/splitflap/element |
defines <split-flap> |
@enavermate/splitflap |
SplitflapBoard (the player) and the planner |
@enavermate/splitflap/planner |
the planner alone, no DOM |
Current Chrome, Firefox and Safari, on desktop and on iPhone: every release plays its demo in Chrome, Firefox and Safari's engine at desktop and iPhone sizes.
React
import { Splitflap } from '@enavermate/splitflap/react';
function Gate({ status }: { status: string }) {
return (
<Splitflap
text={status}
transition="flip"
cellWidth="uniform"
style={{ font: '700 32px ui-monospace, monospace', color: '#f2c230' }}
/>
);
}A new text turns in from the one on screen. The component is a client component ('use client')
and renders a <span>: pass className, style, id and other span attributes as usual.
HTML — no build step: the element straight from a CDN
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@enavermate/splitflap@0/dist/element.js"
></script>
<split-flap class="gate" transition="flip">GATE A3</split-flap>With a bundler, import '@enavermate/splitflap/element' once anywhere does the same.
Change the text attribute (or the text property) and the board turns to it. Without a text
attribute the element's own text is the board's.
Vue — tell the compiler split-flap is a custom element:
// vite.config.js
vue({
template: {
compilerOptions: { isCustomElement: (tag) => tag === 'split-flap' },
},
});<script setup>
import { ref } from 'vue';
import '@enavermate/splitflap/element';
const gate = ref('GATE A3');
</script>
<template><split-flap :text="gate" transition="flip"></split-flap></template>Svelte
<script>
import '@enavermate/splitflap/element';
let gate = $state('GATE A3');
</script>
<split-flap text={gate} transition="flip"></split-flap>Angular — allow custom elements in the component:
import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
import '@enavermate/splitflap/element';
@Component({
selector: 'app-gate',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<split-flap [attr.text]="gate()" transition="flip"></split-flap>`,
})
export class Gate {
gate = signal('GATE A3');
}Anything else
import { SplitflapBoard } from '@enavermate/splitflap';
const board = new SplitflapBoard(document.getElementById('gate')!, {
text: 'MADRID',
});
board.update({ text: 'LISBOA', transition: 'roll' });transition |
What every changed letter does |
|---|---|
reel (default) |
Spins through the alphabet between the old letter and the new one, like a slot machine reel |
roll |
Rolls through a few letters: its neighbours when the new one is close, random ones when it is far |
flip |
Flips over in halves, like a departure board |
scramble |
Flickers through random letters of its own script, then locks |
flip paints its flaps solid so the half they cover never shows through. The board takes their
colour from the first opaque background around it; pass surfaceColor when the background is an
image or a gradient. reel and roll take a direction: random (default — every other cell from
above), auto (each cell the way its letter lies in the alphabet), up, down.
cellWidth |
Behaviour |
|---|---|
natural |
A cell moves from the old glyph's width to the new one's, as text reflows. reel and roll only |
uniform |
Every cell as wide as the alphabet's widest letter, each glyph centred — a station board |
Without one, reel and roll reflow like text, and flip and scramble keep every letter close to
its own width — narrow, regular and wide cells measured from the text's alphabets — since they show
two glyphs in one cell at once and must not reflow.
The React props, the element's attributes and the player's options are the same settings.
| Option | Attribute | Default | What it sets |
|---|---|---|---|
text |
text |
— | The text; the only one a board needs |
transition |
transition |
reel |
reel, roll, flip, scramble |
surfaceColor |
surface-color |
from CSS | The colour under the text, for flip's flaps |
cellWidth |
cell-width |
per transition | natural (reel, roll), uniform |
direction |
direction |
random |
reel and roll: random, auto, up, down |
duration, stagger |
duration, stagger |
650, 40 | Ms per cell (flip takes 1.6×), ms between consecutive changed cells |
staggerOrder |
stagger-order |
ltr |
ltr, rtl, random |
alphabet |
alphabet |
auto |
auto, a script (greek), or { letters: '0123456789ABCDEF' } (attribute: the letters) |
loading |
loading |
false | Random words until it turns false; the real text then lands |
loadingLength |
loading-length |
text ±1 | 6 or { min: 4, max: 10 } (attribute: 4-10) |
seed |
seed |
1 | The same seed plays the same way |
contentKey |
content-key |
— | Replans when it changes even if the text did not: a reused list row |
animateOnMount |
animate-on-mount |
true | false shows the first text, and the first after contentKey changes, at once |
reduceMotion |
reduce-motion |
system |
system follows prefers-reduced-motion; always, never |
tabularDigits |
tabular-digits |
on | Digits of one width, so numbers keep still; off with font-variant-numeric: proportional-nums too |
onTransitionStart, onTransitionEnd |
splitflap-transition-start, splitflap-transition-end events |
— | { text } and { text, interrupted } (an event's detail) |
The font, size, weight, colour, line height and letter spacing are the element's CSS. After a change
the board cannot observe (a class toggled on an ancestor), call refresh() on the element or the
player; font loading is picked up by itself. SplitflapBoard also has update(options) and
destroy(), which puts the plain text back.
The planner — createBoard(), planTransition(board, input) and loadingPlan(length, seed, options)
return every cell's path, timing and direction as plain data
(@enavermate/splitflap/planner, no DOM), so a board's behaviour can be tested without rendering, or
drawn some other way.
A cell rolls through the alphabet of its own letter's script, read from the text — no language setting. A word travels only through the core letters and the extra letters it contains, so a Ukrainian word never flashes a letter only Belarusian uses. Chinese, Korean and anything else without an alphabet changes in one step: no script can break a board. Right-to-left scripts are not supported.
On a server the React component renders <span>{text}</span>; <split-flap> with its text inside is
plain HTML too. In jsdom and happy-dom, which have no canvas, the board stays plain text and reports
every change as finished, so the tests of an app that uses it run unchanged.
The same board, plan for plan:
react-native-splitflap for React Native and
Expo (npx expo install @enavermate/react-native-splitflap), and
SplitflapKit for SwiftUI and UIKit. All three share one
planner, checked against the same 131 golden scenarios, so a text animates the same way everywhere.
How it is built: scivi.dev/oss/react-native-splitflap.
Found a bug, or missing something? Open an issue. See CONTRIBUTING; everyone taking part follows the code of conduct.
MIT © Roman Diukachov