React primitives for @chenglou/pretext — a DOM-free text measurement library that uses the Canvas font engine to compute precise text layout without touching the DOM.
~0.09ms per layout call. No reflow. No layout thrashing.
/— Interactive playground: adjust font, container width, line height, and max lines in real time/landing— Creative showcase of 8 text effects only possible with synchronous, DOM-free measurement
npm install
npm run devOpen http://localhost:3000 for the playground, or http://localhost:3000/landing for the effects showcase.
All hooks are in lib/pretext/ and exported from lib/pretext/index.ts.
The main all-in-one hook. Prepares and lays out text, returning per-line data.
const { lines, height, lineCount } = useTextLines(
"Hello world",
"16px sans-serif",
containerWidth,
24
);Returns { lines: Array<{ text, width }>, height, lineCount, prepared }.
Memoized preparation step — use when you want to share a PreparedTextWithSegments across multiple layout calls (e.g. useTextLayout + layoutNextLine).
const prepared = usePreparedText("Hello world", "16px sans-serif");Layout only — takes an already-prepared text object. Returns { height, lineCount }.
ResizeObserver-based container width tracking. Returns 0 on initial render (before the observer fires).
const containerRef = useRef<HTMLDivElement>(null);
const width = useContainerWidth(containerRef);Render prop component. Measures text at the given width and exposes layout info to children.
<MeasuredText text="Hello" font="16px sans-serif" maxWidth={400} lineHeight={24}>
{({ height, lineCount, measuredWidth }) => (
<div style={{ height }}>measured: {lineCount} lines</div>
)}
</MeasuredText>Props: text, font, maxWidth, lineHeight, style?, options?, children: (info: MeasuredTextInfo) => ReactNode
Renders each line of text, optionally with a custom renderLine callback.
<TextLines
text="Hello world"
font="16px sans-serif"
maxWidth={400}
lineHeight={24}
renderLine={({ text, width, index, isFirst, isLast }) => (
<div key={index} style={{ opacity: isLast ? 0.5 : 1 }}>{text}</div>
)}
/>Props: text, font, maxWidth, lineHeight, options?, renderLine?: (line: LineInfo) => ReactNode
Virtualized rendering for long text — only renders lines in the visible viewport.
<VirtualText
text={longText}
font="16px sans-serif"
maxWidth={600}
lineHeight={24}
containerHeight={400}
onVisibleRangeChange={({ start, end }) => console.log(start, end)}
/>Props: text, font, maxWidth, lineHeight, containerHeight, options?, onVisibleRangeChange?
Finds the narrowest container width that doesn't add extra lines vs. the natural layout — producing visually balanced line breaks.
<BalancedText
text="A headline that should break evenly"
font="32px Georgia"
maxWidth={800}
lineHeight={44}
/>Props: text, font, maxWidth, lineHeight, options?, className?, style?
Truncates to N lines with an ellipsis, with an optional expand/collapse toggle.
<TruncatedText
text={longText}
font="16px sans-serif"
maxWidth={400}
lineHeight={24}
maxLines={3}
expandable
/>Props: text, font, maxWidth, lineHeight, maxLines, options?, expandable?, className?
Context provider for default font and lineHeight values.
<PretextProvider value={{ font: "16px Inter", lineHeight: 26 }}>
{/* components read defaults from context */}
</PretextProvider>Eight text effects at /landing — all built without external animation libraries (CSS transitions, requestAnimationFrame, and IntersectionObserver only):
| # | Effect | Technique |
|---|---|---|
| 1 | Kinetic Line Reveal | IntersectionObserver triggers staggered fade + slide with cubic-bezier spring easing |
| 2 | Mouse-Reactive Text | Imperative rAF loop — no React re-renders on mousemove; translateX/scale/glow by proximity |
| 3 | Fluid Reflow | CSS width transition + ResizeObserver continuously re-layouts text during animation |
| 4 | Canvas Glow Effects | Canvas 2D with dual glow layers, animated hue-shift gradients, DPR scaling |
| 5 | Balanced Headlines | Side-by-side BalancedText vs unbalanced, with pixel-accurate width annotations |
| 6 | Shaped Text | layoutNextLine() with per-row maxWidth from circle/diamond/wave shape functions |
| 7 | Scroll Word Reveal | 280vh sticky section, word-by-word reveal synced to scroll progress |
| 8 | Split-Screen Comparison | Draggable divider, both panels reflow in real time as serif vs monospace widths change |
Traditional text measurement forces browser layout (reflow). @chenglou/pretext runs the same Canvas font engine used by browsers — synchronously, in JS, without any DOM interaction. This enables:
- Layout during render without side effects
- Continuous reflow during CSS animations (Section 3)
- Imperative per-frame layout in rAF loops (Section 2)
- Server-side text layout (with
OffscreenCanvasor Node canvas) - Virtualization based on exact line counts (Section 7)
- Next.js 16 (App Router)
- React 19 with React Compiler
- TypeScript 5
- Tailwind CSS 4
@chenglou/pretext