Start
//

Video Composition

Text Element Concepts

Conceptual explanations for how ef-text works internally. Read these when you want to understand why something behaves the way it does, not just how to use it.


Text Splitting: Locale-Aware and Grapheme-Aware Segmentation

ef-text uses the browser's Intl.Segmenter API to split text intelligently across languages and complex characters.

Why naive splitting fails

  • Splitting on spaces breaks languages without spaces (Chinese, Japanese, Thai)
  • Splitting by character index breaks with emoji, combining characters, and multi-byte sequences
  • Manual regex patterns miss edge cases across languages

Examples of failure modes:

  • "Hello 👋 world".split("") creates 13 "characters" — the emoji spans multiple code points
  • "你好世界".split(" ") produces one segment (no spaces in Chinese)
  • Manual word detection misses contractions, hyphenated words, and punctuation

How Intl.Segmenter fixes this

Word splitting (split="word")Intl.Segmenter with granularity: "word":

  • Proper word boundary detection for languages without spaces
  • Identifies word-like segments vs. punctuation and whitespace
  • Preserves whitespace segments for correct spacing

Character splitting (split="char")Intl.Segmenter with granularity: "grapheme":

  • Emoji are treated as a single grapheme cluster, not multiple code points
  • Combining characters (e.g., "é" as "e" + combining mark) handled correctly
  • Works with complex scripts (Arabic, Indic, etc.)

Line splitting (split="line") — splits on \r?\n, trims whitespace, filters empty lines.

Benefits

  • Works with any language the browser supports — no manual detection needed
  • Mixed-language text (e.g., "Hello 你好 world") is handled seamlessly
  • Visual character count matches actual display count

Word Boundary Preservation: Preventing Line Breaks in Character Mode

When split="char" is used, ef-text automatically detects word boundaries and wraps characters within words to prevent mid-word line breaks.

Why this matters

When text is split into individual characters, each character becomes a separate element. Without grouping, characters can break across lines mid-word:

Hel
lo
wor
ld

instead of:

Hello
world

How word wrapping works

  1. Text is split into characters using grapheme-aware segmentation
  2. Word boundaries are detected using Intl.Segmenter with granularity: "word"
  3. Characters are grouped by word index
  4. Each word group is wrapped in a <span class="ef-word-wrapper"> with display: inline-block; white-space: nowrap
  5. Words can wrap to new lines; characters within a word never break

Benefits

  • Words never break across lines — reading flow is preserved
  • Each character is still a separate, independently animatable segment
  • Stagger timing and the template pattern work per character
  • No configuration required; works automatically for any language

Stagger and Easing: Non-Linear Timing Patterns

ef-text combines stagger delays with easing functions to create timing patterns where segments can cluster or spread unevenly, rather than appearing at perfectly fixed intervals.

The problem with fixed delays

With uniform index × delay offsets, segments appear at 0ms, 50ms, 100ms, 150ms — mechanical and often unnatural-feeling.

How eased stagger distribution works

Each segment's index is normalized to a 0–1 range (first = 0, last = 1). An easing function is applied to this normalized progress, and the eased value is multiplied by the total stagger duration.

Easing functions and their effects:

ValueEffect
"linear"Even distribution (default)
"ease-out"Fast start, slow end — segments cluster early, spread later
"ease-in"Slow start, fast end — segments spread early, cluster later
"ease-in-out"Slow start and end, fast middle
"cubic-bezier(x1, y1, x2, y2)"Precise custom control

Example with ease-out on 10 segments at stagger="50ms" (450ms total):

  • Segment 0: eased(0) × 450ms = 0ms
  • Segment 5: eased(0.5) × 450ms ≈ 300ms (clustered early)
  • Segment 9: eased(1) × 450ms = 450ms

The --ef-stagger-offset CSS variable on each segment holds its calculated delay, ready to use with animation-delay.

Benefits

  • ease-out creates the accelerating-reveal feel common in modern UI animation
  • Matches timing patterns users expect
  • Uses the same easing curve vocabulary as CSS — easy to align with other animated elements
  • Custom bezier enables exact choreography

Template Pattern: Declarative Multi-Segment System

ef-text uses a declarative <template> element to clone multiple visual segments per text unit without requiring JavaScript.

The problem with single segments

To create layered effects (shadows, outlines, glows), you'd normally need to:

  • Manually duplicate each character in JavaScript
  • Position duplicate elements relative to their originals
  • Coordinate timing between duplicate hierarchies

How template cloning works

Place a <template> element inside <ef-text> containing one or more <ef-text-segment> elements. For each text unit (character, word, or line), the template content is cloned and each clone receives the same text content and timing properties.

<ef-text split="char">
<template>
<ef-text-segment class="shadow" />
<ef-text-segment class="main" />
</template>
Hello
</ef-text>

This creates two segments per character — one for the shadow layer, one for the main text — with no JavaScript.

Benefits

  • Declarative — pure HTML, no scripting needed
  • Automatic synchronization — all segments in a clone share the same stagger offset and timing
  • Any number of layers — add as many ef-text-segment elements to the template as needed
  • Works with any split mode — char, word, or line
  • Efficient — browser-native template cloning, single DOM pass

Deterministic Randomness: Consistent Variation Across Renders

The --ef-seed CSS variable provides each text segment with a deterministic pseudo-random value (0–1) based on its index, enabling organic-looking variation that is identical on every render.

Why Math.random() breaks video rendering

// Different every time — breaks video rendering
color: `hsl(${Math.random() * 360}, 80%, 70%)`;

Each page load and each rendered video frame produces different values. Frames of the same composition look different, making reproducible video output impossible.

How deterministic seeds work

Each segment's seed is calculated from its index using a deterministic algorithm — no Math.random(), no external randomness. The same segment index always produces the same seed value, across browsers, page loads, and render passes.

The --ef-seed variable:

  • Available on every ef-text-segment element
  • Ranges from 0 to 1
  • Distributed pseudo-randomly across segments (not linearly)
  • Consistent in browser preview and server-side rendering

Mapping seed to visual properties with calc():

/* Hue (0–360°) */
color: hsl(calc(360 * var(--ef-seed)), 80%, 70%);
/* Scale (1.0–1.3) */
transform: scale(calc(1 + 0.3 * var(--ef-seed)));
/* Rotation (−10deg to +10deg) */
transform: rotate(calc(var(--ef-seed) * 20deg - 10deg));

Benefits

  • Consistent rendering — same text always produces the same visual result
  • No JavaScript needed — pure CSS with calc()
  • Render-safe — values are determined at paint time, not animation time
  • Varied appearance — organic feel without the unpredictability of true randomness