Alphabet Ledger
An alphabetical index scroll — a poster-scale initial letter is the sticky anchor of each section, and at the handover the new letter pushes the previous one out with a thin-to-bold weight baton animation.
npx shadcn@latest add https://webberui.com/r/alphabet-ledger.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<AlphabetLedger />
Installation
npx shadcn@latest add https://webberui.com/r/alphabet-ledger.jsonOr, once registries are configured in components.json, install it as @webberui/alphabet-ledger.
Usage
import {
AlphabetLedger,
AlphabetLedgerSection,
} from "@/components/ui/alphabet-ledger";
<AlphabetLedger>
<AlphabetLedgerSection letter="A" label="Animation">
<p>The content scrolls alongside the giant letter……</p>
</AlphabetLedgerSection>
<AlphabetLedgerSection letter="B" label="Baton">
<p>Scroll to here and B pushes A out, picking up the bold weight.</p>
</AlphabetLedgerSection>
</AlphabetLedger>If the scrolling happens inside a nested overflow-y-auto container, pass that container's ref to container:
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[320px] overflow-y-auto">
<AlphabetLedger container={scrollerRef}>{/* ... */}</AlphabetLedger>
</div>Props
AlphabetLedger
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Put AlphabetLedgerSections here, one per letter section |
showIndex | boolean | true | Whether to show the clickable letter index scroll on the right |
thinWeight | number | 200 | Font weight of an inactive letter (thin) |
boldWeight | number | 800 | Font weight of the active letter (bold), reached at the instant of the handover |
letterSizeClassName | string | "text-[5.5rem] leading-none sm:text-[8rem]" | Type-size classes for the giant letter |
letterClassName | string | — | Overrides the giant letter's styles (colour, tracking, and so on) |
container | RefObject<HTMLElement | null> | — | Nested scroll container ref, used for pin detection and as the scroll target |
onActiveChange | (index: number, letter: string) => void | — | Callback when the active section changes |
className | string | — | Appended to the outer container |
AlphabetLedgerSection
| Prop | Type | Default | Description |
|---|---|---|---|
letter | string | — | The giant initial letter used as the section anchor |
children | ReactNode | — | Section content, which scrolls alongside the letter |
label | ReactNode | — | Small caption under the letter; when it is a string it also serves as the section's accessible name |
id | string | — | Anchor id applied to the section |
className | string | — | Appended to the section container |
How it works
- Each section's giant initial letter is pinned to the top with
position: sticky; top: 0, and its movement is bounded by its own section; when a section scrolls out, the next section's letter pins immediately, visually "pushing" the previous one out. - At the instant of the handover, the incoming letter's
fontWeightclimbs fromthinWeighttoboldWeight(paired with a slight scale) while the previous one falls back to the thin weight, producing the "baton" animation. How smooth that weight transition looks depends on whether the typeface is a variable-weight font (variable font). - The active section is decided by an
IntersectionObserver: the deepest section that has passed the top pinning line is the active one, and the index scroll on the right and the font weights update in sync. - When
containeris passed, both the pin detection (theIntersectionObserverroot) and the index scroll's scroll positioning switch to that container; otherwise the window is the scroll container.
Accessibility
- The giant letter is a decorative visual anchor and is marked
aria-hidden; please provide a real heading in the section content, and whenlabelis a string it becomes the<section>'saria-label. - The index scroll on the right consists of native
<button>s that are keyboard-focusable and triggered with Enter, carrying anaria-labelandaria-currentto mark the current section. - When the user has "reduce motion" enabled at the system level, the sticky stacking layout is fully preserved and only the weight baton transition and smooth scrolling are disabled (it jumps into position instead).
Glyph Portal Layout
A layout skeleton built from giant letters used as mask windows, each holding its own image or video that moves with parallax on scroll, with small type flowing around them.
Woodtype Flush Block
A woodtype flush heading — each line of a multi-line heading is solved onto the variable font's wdth axis so it stretches flush to both edges of the container, and on reveal the lines spring open from an extremely narrow width to the solved width, one after another.