Skip to content
enavermatePublic

About

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

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

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

npm bundle size license

Live demo

A departures board with cities in their own scripts, the four transitions, numbers, emoji, cell widths and all four directions, all moving at once
Emoji: Twemoji, CC-BY 4.0

✅ 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, roll and scramble, 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 natural and uniform, 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-motion shows 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

About

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

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors