@enavermate/splitflap
v0.5.0
Published
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
Downloads
43
Maintainers
Readme
splitflap
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
✅ Works with
| 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>✨ Features
- 🎰 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
📦 Installation
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.
🚀 Quick start
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' });🎞️ Transitions
| 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.
📏 Cell width
| 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.
⚙️ API
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.
🔤 Scripts
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.
🧪 Tests and server rendering
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.
🌍 Also for React Native and Swift
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.
🤝 Contributing
Found a bug, or missing something? Open an issue. See CONTRIBUTING; everyone taking part follows the code of conduct.
📄 License
MIT © Roman Diukachov
