Text Flapper

Solari split-flap display effect with cycling characters and configurable transition types.

Text Flapper animates text like a mechanical Solari departure board — each character cycles through random intermediate glyphs before landing on its target, with seven built-in transition styles.

Text Flapper automatically applies split: 'chars' when flap is set — you do not need to add it yourself. If you do provide split, it overrides the internal default, letting you reconfigure the split (e.g. split: 'chars,words'). The SDK also auto-adds a default stagger (each: 0, from: 'start') when none is set. The flap config controls the cycling behavior; from/to handle surrounding motion.

Basic Usage

typescript
import { Motion } from "@motion.page/sdk";

// split: 'chars' is auto-applied — no need to add it
Motion("board", ".departure-title", {
  flap: { type: "board", styledBoard: true },
  stagger: 0.05,
  duration: 0.8,
}).onPageLoad();

// Explicit split overrides the default (e.g. to also split words)
Motion("board", ".departure-title", {
  split: "chars,words",
  flap: { type: "board", styledBoard: true },
  stagger: 0.05,
  duration: 0.8,
}).onPageLoad();

The flap Property

Set flap inside your AnimationConfig. All sub-options are optional.

typescript
Motion("reveal", ".hero-text", {
  split: "chars",
  flap: {
    type: "flip",
    charset: "alphanumeric",
    cycles: [2, 5],
    perspective: 400,
  },
  stagger: 0.04,
  duration: 0.6,
}).onPageLoad();
OptionTypeDefaultDescription
typeFlapType'flip'Transition style for each character — see Transition Types
charsetCharsetPreset | string'alphanumeric'Character pool for random intermediate glyphs — see Charsets
cyclesnumber | [min, max][2, 5]How many random chars to show before landing. Tuple = random per character
perspectivenumber400CSS perspective depth in px. Only affects flip and board types
styledBoardbooleantrueDark gradient tiles with split-line divider. Only applies to board type
stableWidthboolean | 'cells' | 'container'falseStrategy for preventing layout shift during cycling — see stableWidth. Auto-enabled (as 'cells') when board + styledBoard: true
preserveWhitespaceCellsbooleanfalseRender spaces as empty board cells. Only applies to board type

Transition Types

Seven styles are available via flap.type. The default is 'flip'.

flip — 3D card rotation

Each character rotates on the X axis like a physical flip card. Composable with from/to transforms.

typescript
Motion("flip-in", ".headline", {
  split: "chars",
  flap: { type: "flip", cycles: 6, perspective: 300 },
  stagger: 0.05,
  duration: 0.8,
}).onPageLoad();

fade — opacity crossfade

The outgoing character fades out, the incoming fades in. fade owns the opacity property — do not set opacity in from/to when using this type.

typescript
Motion("fade-decode", ".status-text", {
  split: "chars",
  flap: { type: "fade", charset: "katakana", cycles: [4, 8] },
  stagger: { each: 0.03, from: "random" },
  duration: 1,
}).onPageLoad();

slide — vertical slide

Characters slide in and out vertically, clipped by an overflow: hidden parent. Composable with other transforms.

typescript
// Slot machine effect
Motion("slot", ".score", {
  split: "chars",
  flap: { type: "slide", charset: "numeric", cycles: 8 },
  stagger: 0.06,
  duration: 1.2,
}).onPageLoad();

blur — blur dissolve

Characters blur in and out. blur owns the filter property — do not set filter in from/to when using this type.

typescript
Motion("blur-decode", ".classified", {
  split: "chars",
  flap: { type: "blur", cycles: [3, 6] },
  stagger: 0.03,
  duration: 0.8,
}).onPageLoad();

scale — scale pop

Characters scale in and out. Composable with other from/to transforms.

typescript
Motion("scale-unlock", ".success-text", {
  split: "chars",
  flap: { type: "scale", cycles: [2, 4] },
  stagger: 0.04,
  duration: 0.7,
}).onPageLoad();

board — Solari departure board

Full mechanical split-flap effect: each character element is restructured into layered panels that fold in half. The most realistic — use with styledBoard: true for the complete dark-tile look.

typescript
// Styled departure board
Motion("departures", ".board-title", {
  split: "chars",
  flap: { type: "board", styledBoard: true, perspective: 400 },
  stagger: 0.05,
  duration: 0.9,
}).onPageLoad();

// Unstyled — inherit your own CSS
Motion("gate", ".gate-code", {
  split: "chars",
  flap: { type: "board", styledBoard: false },
  stagger: 0.04,
  duration: 0.7,
}).onPageLoad();

Board type ownership: restructures the character element’s DOM into child panels. The original el.textContent is cleared; text lives in the generated sub-elements.

none — text cycling only

No built-in visual transition. Characters cycle through the charset without animation. Use this with your own from/to properties to fully control the effect.

typescript
// Pure text scramble — visuals come from from/to
Motion("scramble", ".encrypted", {
  split: "chars",
  flap: { type: "none", charset: "symbols", cycles: [6, 12] },
  from: { opacity: 0 },
  stagger: 0.02,
  duration: 0.6,
}).onPageLoad();

Charsets

The charset option sets the pool of random intermediate characters. Pass a preset name or any string — every character in the string is drawn from at random.

PresetCharacters
'alphanumeric'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
'alpha'ABCDEFGHIJKLMNOPQRSTUVWXYZ
'numeric'0123456789
'hex'0123456789ABCDEF
'binary'01
'katakana'Full katakana syllabary ()
'symbols'!@#$%^&*+-=<>?/|~
'blocks'░▒▓█▄▀■□▪▫
custom stringAny string — each character is used as a random intermediate
typescript
// Custom charset — Greek letters
Motion("greek", ".formula", {
  split: "chars",
  flap: { type: "flip", charset: "αβγδεζηθικλμνξοπρστυφχψω" },
  stagger: 0.04,
  duration: 0.8,
}).onPageLoad();

// Matrix effect
Motion("matrix", ".system-status", {
  split: "chars",
  flap: { type: "fade", charset: "katakana", cycles: [4, 8] },
  stagger: { each: 0.03, from: "random" },
  duration: 1,
}).onPageLoad();

Cycles

cycles controls how many random characters appear before landing on the target. Use a number for a fixed count or a [min, max] tuple for a random range per character — the variation makes the effect feel more organic.

typescript
// Fixed — every char does exactly 6 cycles
flap: { cycles: 6 }

// Range — each char gets 2–8 random cycles
flap: { cycles: [2, 8] }

The builder exposes this as two separate sliders (min / max), range 1–20.

Combining flap with from/to

For flip, slide, scale, and none types, the from/to properties animate once per cycle using a V-curve:

  • Phase 0→0.5: value moves from to toward from (character becoming hidden)
  • Phase 0.5→1: value moves from from back to to (new character revealed)

This means from: { opacity: 0, y: 20 } rides every cycle, not just the first.

typescript
// Flip + additional opacity + translate per cycle
Motion("flip-reveal", ".hero-text", {
  split: "chars",
  flap: { type: "flip", cycles: 4 },
  from: { opacity: 0, y: 20 },
  stagger: 0.04,
  duration: 0.8,
  ease: "power2.out",
}).onPageLoad();

Property conflicts to avoid:

TypeOwned propertyDon’t use in from/to
fadeopacityopacity
blurfilterfilter
fliprotateX (composable)safe to add other transforms
slidetranslateY (composable)safe to add other transforms
scalescale (composable)safe to add other transforms

The Board Type in Detail

type: 'board' restructures each character element into layered sub-elements that replicate the mechanics of a physical split-flap display:

plaintext
container
├─ staticTop     — top half of incoming character (revealed as flap falls)
├─ staticBottom  — shows old char bottom until midpoint, then new char
├─ flapWrap      — rotates 0°→-180° around the center split line
│   ├─ flapFront — old character top half (visible 0°–90°)
│   └─ flapBack  — new character bottom half (visible 90°–180°)
├─ shadow        — opacity driven by sin(phase × π) for depth
└─ splitLine     — permanent 1px center divider (z-index: 3)

styledBoard

When true (the default), applies departure-board tile styling:

  • Dark gradient background (#2a2a2e → #1e1e22 top, #1e1e22 → #16161a bottom)
  • 4px border radius
  • Text color #e8e8ec
  • Split-line color rgba(0,0,0,0.6)
  • 2px margin between tiles

When false, styling is minimal — only a rgba(0,0,0,0.15) split line and 1px margin. Use this to apply your own tile CSS.

preserveWhitespaceCells

By default, spaces and other whitespace are skipped and never cycled. Set preserveWhitespaceCells: true to render them as empty board cells — useful when you need fixed-width rows like a real departure board.

typescript
Motion("flight-info", ".flight-row", {
  split: "chars",
  flap: {
    type: "board",
    styledBoard: true,
    preserveWhitespaceCells: true,
  },
  stagger: 0.04,
  duration: 1,
}).onPageLoad();

stableWidth

Controls how the flap prevents layout shift while random characters cycle through. Each mode trades a different aesthetic for stability.

ValueBehavior
false (default)No pinning. Text width flexes with each rendered glyph. Fine for same-width charsets (Latin → alphanumeric).
true or 'cells'Pin each character cell to the widest glyph across the target and the charset pool. Every cell is the same width regardless of which glyph renders at the moment. Great for monospace aesthetics (departure boards, counters). Adds visible spacing around narrow Latin letters when paired with wide charsets (katakana, CJK).
'container'Pin the parent element’s outer width only — characters flow naturally inside. Latin letters keep their natural spacing while the surrounding layout (rows, grids) stays rock-steady. Best for inline hover flappers on nav links where cell-pinning would space the letters out unnaturally.

Automatically enabled (as 'cells') when board + styledBoard: true.

typescript
// Cell mode — every cell the same width (classic split-flap look)
Motion("counter", ".score", {
  flap: { type: "fade", charset: "numeric", cycles: 8, stableWidth: "cells" },
  duration: 1,
}).onPageLoad();

// Container mode — outer width reserved, letters stay naturally spaced
Motion("nav-flap", ".nav-link", {
  flap: { type: "fade", charset: "katakana", cycles: [3, 6], stableWidth: "container" },
  duration: 0.5,
}).onHover({ each: true, onEnter: "restart", onLeave: "none" });

Perspective

perspective sets the CSS 3D perspective depth in pixels. Only affects flip and board types. Lower values create a more dramatic, compressed 3D effect.

typescript
// Dramatic close perspective
flap: { type: "board", perspective: 60, styledBoard: true }

// Subtle, distant perspective (default feel)
flap: { type: "flip", perspective: 400 }

Range: 50–1000 px.

Use Cases

Departure Board

typescript
Motion("departures", ".board-header", {
  split: "chars",
  flap: { type: "board", styledBoard: true },
  stagger: 0.05,
  duration: 0.9,
}).onPageLoad();

Countdown / Score Counter

typescript
Motion("score", ".score-value", {
  split: "chars",
  flap: { type: "slide", charset: "numeric", cycles: 10 },
  stagger: 0.08,
  duration: 1.2,
}).onPageLoad();

Classified / Decode Reveal

typescript
Motion("classified", ".secret-text", {
  split: "chars",
  flap: { type: "blur", cycles: [3, 6] },
  stagger: 0.03,
  duration: 0.9,
}).onScroll({ toggleActions: "play none none none", start: "top 80%" });

Cyber / Matrix Aesthetic

typescript
Motion("matrix", ".terminal-output", {
  split: "chars",
  flap: { type: "fade", charset: "katakana", cycles: [4, 8] },
  stagger: { each: 0.03, from: "random" },
  duration: 1,
}).onPageLoad();

Binary Counter

typescript
Motion("binary", ".bit-display", {
  split: "chars",
  flap: { type: "slide", charset: "binary", cycles: 10 },
  stagger: 0.08,
  duration: 1.2,
}).onPageLoad();

Blocks Loading Indicator

typescript
Motion("loading", ".loading-text", {
  split: "chars",
  flap: { type: "none", charset: "blocks", cycles: [4, 8] },
  stagger: 0.02,
  duration: 0.8,
}).onPageLoad();

Stagger with Flapper

Stagger is the primary way to shape the flapper’s cascade feel. All stagger options work — from: 'random' is particularly effective for organic decode effects.

typescript
// Left-to-right cascade (default)
stagger: 0.05

// Random order — each char starts at a different time
stagger: { each: 0.03, from: "random" }

// Center outward
stagger: { each: 0.04, from: "center" }

Cleanup

The board type restructures the DOM. Use Motion.reset() or .kill() to revert character elements back to their original state.

typescript
// Kill one timeline and revert DOM
Motion("departures").kill();

// Revert by target element
Motion.reset(".board-title");

API Reference

typescript
interface FlapConfig {
  type?: 'flip' | 'fade' | 'slide' | 'blur' | 'scale' | 'board' | 'none';
  charset?: 'alphanumeric' | 'alpha' | 'numeric' | 'hex' | 'binary' | 'katakana' | 'symbols' | 'blocks' | string;
  cycles?: number | [min: number, max: number];
  perspective?: number;
  styledBoard?: boolean;
  stableWidth?: boolean | 'cells' | 'container';
  preserveWhitespaceCells?: boolean;
}

// Used inside AnimationConfig — split: 'chars' is auto-applied when flap is set
interface AnimationConfig {
  split?: SplitType;  // optional — defaults to 'chars' when flap is present
  flap?: FlapConfig;
}

Related: Text Splitter (optional — auto-applied by flap) · Stagger · Opacity