Leading Unfold
Narrative through line height — a paragraph starts compressed into a near-solid block of ink, and scroll progress gradually unfolds the leading and tracking into a readable paragraph, growing the layout height and pushing the following sections down.
npx shadcn@latest add https://webberui.com/r/leading-unfold.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<LeadingUnfold />
Installation
npx shadcn@latest add https://webberui.com/r/leading-unfold.jsonOr, once registries are configured in components.json, install it as @webberui/leading-unfold.
Usage
import { LeadingUnfold } from "@/components/ui/leading-unfold";
<LeadingUnfold className="text-lg font-medium">
The paragraph starts compressed into a blot of ink and gradually unfolds into a readable passage as you scroll.
</LeadingUnfold>The component creates a useScroll from its own position within the scroll container, and as its top edge enters the configured trigger range, line-height and letter-spacing interpolate from the compressed state to the expanded state. If the text lives inside a nested overflow scroll container, pass that container's ref to container:
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[300px] overflow-y-auto">
<LeadingUnfold container={scrollerRef}>{longText}</LeadingUnfold>
</div>Controlled mode
Pass in an external progress (a 0 → 1 MotionValue) to drive the unfolding yourself — for example bound to a page-level useScroll or any other animation source:
const { scrollYProgress } = useScroll();
<LeadingUnfold progress={scrollYProgress}>{longText}</LeadingUnfold>Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The paragraph of text to unfold |
compressedLeading | number | 0.32 | Line-height multiple in the compressed state; lower overlaps more into a block of ink |
expandedLeading | number | 1.75 | Line-height multiple in the expanded state (the final readable paragraph) |
compressedTracking | number | -0.055 | Tracking in the compressed state (em); negative values strengthen the ink-block feel |
expandedTracking | number | 0 | Tracking in the expanded state (em) |
blur | boolean | true | Whether a slight blur accompanies the compressed state, dissipating as it unfolds |
blurAmount | number | 3 | Maximum blur radius in the compressed state (px) |
offset | ScrollOffset | ["start 0.85", "start 0.4"] | Overrides the useScroll trigger range |
progress | MotionValue<number> | — | Controlled mode: an external 0→1 progress drives the unfolding |
container | RefObject<HTMLElement | null> | — | Nested scroll container ref; defaults to the window as the scroll container |
as | "p" | "div" | "blockquote" | "h2" | "h3" | "p" | The semantic tag to render |
className | string | — | Additional styles (size, weight, colour, and so on) |
How it works
- Both ends of the trigger range are anchored to the block's top edge (
start), so the bottom edge growing downward never doubles back and interferes with its own progress calculation, keeping the unfolding smooth and one-way. - Animating
line-heighttriggers reflow every frame — this is exactly where the "layout height grows and pushes the following sections down" effect comes from; use it on only a few narrative blocks per view. - Font size, weight, and colour are all controlled by
className; the component itself only handles the interpolation of leading, tracking, and blur.
Accessibility
- The text is a real DOM node and the compressed state is purely visual styling, so a screen reader always reads the complete, correct paragraph.
- When the user has "reduce motion" enabled at the system level, the final readable paragraph is rendered statically in its expanded state and no scroll animation is mounted at all.
Overprint Register
A heading rendered as several overprinted colour plates that slide in from misalignment into exact register when they enter the viewport, then drift slightly out of register again on hover.
Lineup Poster Section
A festival-poster lineup layout — names are grouped into rows by tier with the size stepping down each level, and hovering or clicking one promotes it to a bigger row with a shared layout animation while the rest re-justify to fill the width.