WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/leading-unfold.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

0.32
1.75
-0.055
3
<LeadingUnfold />

Installation

npx shadcn@latest add https://webberui.com/r/leading-unfold.json

Or, 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

PropTypeDefaultDescription
childrenReact.ReactNodeThe paragraph of text to unfold
compressedLeadingnumber0.32Line-height multiple in the compressed state; lower overlaps more into a block of ink
expandedLeadingnumber1.75Line-height multiple in the expanded state (the final readable paragraph)
compressedTrackingnumber-0.055Tracking in the compressed state (em); negative values strengthen the ink-block feel
expandedTrackingnumber0Tracking in the expanded state (em)
blurbooleantrueWhether a slight blur accompanies the compressed state, dissipating as it unfolds
blurAmountnumber3Maximum blur radius in the compressed state (px)
offsetScrollOffset["start 0.85", "start 0.4"]Overrides the useScroll trigger range
progressMotionValue<number>Controlled mode: an external 0→1 progress drives the unfolding
containerRefObject<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
classNamestringAdditional 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-height triggers 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.

On this page