Documentation
Chop is the small layer between real browser text and animation code. It measures with Pretext, projects animation handles, and leaves the source DOM in place for accessibility.
Install
Section titled “Install”pnpm add @tonybonet/chopimport { chop } from '@tonybonet/chop'import gsap from 'gsap'
const words = chop('.headline')
gsap.from(words, { y: 32, opacity: 0, stagger: 0.05, ease: 'power3.out',})The returned value is already the word array. That keeps the common case short and lets animation libraries consume the result directly.
Unit Access
Section titled “Unit Access”const title = chop('.headline')
title // word elementstitle.lines // lazy line elementstitle.chars // lazy grapheme elementstitle.paragraphs // lazy paragraph elementstitle.meta // layout counts and dimensionsWords project immediately because they are the default animation unit. Lines, characters, and paragraphs project on first access.
| Unit | Use it when |
|---|---|
| Words | You want fast headline staggers with minimal DOM projection. |
| Lines | The animation needs real rendered line breaks. |
| Characters | You need grapheme-aware motion for emoji, accents, symbols, or mixed scripts. |
| Paragraphs | You want section-level reveals without reparsing blank-line boundaries. |
The playground is built to exercise the API surface, not just look pretty.
| Demo | What to inspect |
|---|---|
| Words | Default projection, replay behavior, and GSAP stagger timing. |
| Lines | Resolved line handles on real wrapping text. |
| Chars | Emoji, punctuation, accents, and non-Latin samples without manual slicing. |
| Split | Unit switching and editable text without stale overlays. |
| Compose | select() queries like middle characters, ranges, and grouped selections. |
| Metrics | Browser-derived x-height, cap-height, ascender, and descender data. |
| Power | Split cost, animation cost, and heavier DOM projection scenarios. |
React Pattern
Section titled “React Pattern”Mount from a callback ref. Destroy the previous instance before creating the next one so React owns the source DOM and Chop owns only its overlay.
import { chop, type ChopElements } from '@tonybonet/chop'import { useCallback, useRef } from 'react'
function Heading() { const instanceRef = useRef<ChopElements | null>(null)
const setTextNode = useCallback((node: HTMLHeadingElement | null) => { instanceRef.current?.destroy() instanceRef.current = node ? chop(node) : null }, [])
return <h1 ref={setTextNode}>Text with real layout</h1>}Keep ChopElements in a ref, not component state. It is an imperative DOM
projection, so state just adds rerenders without clarifying ownership.
Pure Layout
Section titled “Pure Layout”Use layoutText() when a renderer should own the markup.
import { defineFont, layoutText, select } from '@tonybonet/chop'
const result = layoutText( 'Slice cleanly across scripts: Hola, 東京, emoji 👋', defineFont('700 48px Inter'), { width: 520 },)
const middle = select(result.chars, { mode: 'middle', count: 7 })Pure mode returns measured handles, not DOM nodes. It still needs a browser-like canvas environment because Pretext measures text.
Relayout
Section titled “Relayout”Chop keeps the source DOM readable and owns only the overlay. When text, fonts,
or container width change, call relayout() before replaying animation.
const title = chop('.headline')
await document.fonts.readytitle.relayout()Lifecycle summary:
| Method | Effect |
|---|---|
chop() | Creates one aria-hidden overlay and projects words. |
relayout() | Recomputes handles and updates the overlay in place. |
destroy() | Removes the overlay and restores source styles. |
Power Benchmark
Section titled “Power Benchmark”The Power demo separates split work from animation work.
| Track | What it proves |
|---|---|
| Chop lazy | Words can animate without projecting lines or characters up front. |
| Chop full | Word, character, line, and paragraph handles are available when needed. |
| Manual DOM | Raw DOM can be fast, but ownership and escaping are yours. |
| GSAP SplitText | A mutation-based reference point for production animation workflows. |
Read the benchmark as a workflow comparison, not a universal scoreboard. Different text, fonts, container widths, and animation effects will move the numbers.
API Shape
Section titled “API Shape”Before adding API, check the shape:
| Question | Good answer |
|---|---|
| Common word animation | gsap.from(chop('.hero'), options) |
| Lines, chars, or paragraphs | title.lines, title.chars, title.paragraphs |
| Filtering | title.filter((_, i) => i % 2 === 0) |
| Pure measurement | layoutText(text, font) |
No by, no elements(), no duplicate .words. The array is already the words.
Migration
Section titled “Migration”| Old | New |
|---|---|
chop(node, { by: ['word'] }).elements('word') | chop('.headline') |
instance.elements('line') | title.lines |
instance.elements('char') | title.chars |
instance.elements('paragraph') | title.paragraphs |
instance.meta() | title.meta |
instance.refresh() | title.relayout() |
chop(text, font) | layoutText(text, font) |
- Single line breaks stay inside one paragraph. Blank lines create new paragraph handles.
- Locale-sensitive transforms infer
langfrom the element unless you passlocaleexplicitly. destroy()removes the overlay and allows the same target to mount again.- GitHub Pages deploys with
GITHUB_PAGES=trueso Astro emits URLs under/chop.