Marginalia Article
Tufte-style sidenote reading layout — a main column paired with a fixed-width margin column, superscript numbers in the body connected to their sidenotes by hairlines, sidenotes fading in and highlighting in sync as you scroll, and an automatic fallback to inline expandable notes on narrow screens.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
npx shadcn@latest add "https://webberui.com/r/marginalia-article.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<MarginaliaArticle />
Installation
npx shadcn@latest add "https://webberui.com/r/marginalia-article.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/marginalia-article.
Usage
Wrap the phrase you want to mark in Sidenote inside the body text and put the sidenote content in the note prop; the numbers are generated automatically in reading order.
import {
MarginaliaArticle,
Sidenote,
} from "@/components/ui/marginalia-article";
<MarginaliaArticle>
<p>
Tufte's layout pushes supplementary commentary{" "}
<Sidenote note="The reader only has to glance right — no need to move their eyes to the bottom of the page.">
outside the main column
</Sidenote>
, keeping the body text on a clean reading path.
</p>
</MarginaliaArticle>;If the article sits inside a nested scroll container, pass that container's ref to container so focus detection works against the correct scroll root:
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[400px] overflow-y-auto">
<MarginaliaArticle container={scrollerRef}>{/* … */}</MarginaliaArticle>
</div>;Props
MarginaliaArticle
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The article content, with Sidenote placed inside the body text |
sidenoteWidth | string | "12rem" | Width of the margin column (any CSS length) |
gap | string | "2.5rem" | Gap between the main column and the margin column |
breakpoint | number | 640 | Below this available article width (px), it falls back to inline expandable notes |
container | React.RefObject<HTMLElement | null> | — | Ref of the nested scroll container; defaults to the window as the scroll root |
className | string | — | Appended to the outermost container |
Sidenote
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The marked phrase in the body text, highlighted when it becomes the focus |
note | React.ReactNode | — | The sidenote content: shown in the margin column on wide screens, expanded inline on narrow ones |
id | string | automatic | Anchor id; generated automatically when omitted, and must be stable for correct pairing |
label | React.ReactNode | automatic number | Overrides the superscript marker (for example ※ instead of a number) |
className | string | — | Appended to the outer span of the in-body marker |
How it works
- Overlap-free stacking: each sidenote aligns to the vertical position of its anchor, and notes that come too close automatically give way downward to preserve a minimum gap.
- Connecting line: a Bézier curve bends smoothly from the right edge of the superscript number into the margin column; the focused note's line darkens, unfocused ones fade out, and anything outside the viewport is hidden entirely.
- When measurement happens: positions and connecting lines are computed relative to the article box, so they are only recalculated when the layout or size changes (
ResizeObserver/resize), never on scroll, which keeps performance steady. - Focus detection: an
IntersectionObservertracks each anchor, and the one closest to the reading line (about 32% of the viewport height) becomes the current focus.
Accessibility
- When the user has "reduce motion" enabled at the system level, the fades, offsets, and connecting-line transitions all degrade to instant switches, with the layout and content unchanged.
- The superscript marker is a real
<button>that can be focused and triggered by keyboard; on wide screens it associates with the margin sidenote viaaria-describedby, and on narrow screensaria-expanded/aria-controlsdescribe the inline expanded state. - The sidenote container carries
role="note"; on narrow screens it falls back to a tappable inline expandable note, so small screens still get the full context. - Every
IntersectionObserver,ResizeObserver, and event listener is fully cleaned up on unmount.
Leader Line Captions
Magazine-style leader-line callouts — numbered captions arranged along a sidebar, connected by SVG leader lines to detail points in the image, drawn one by one on reveal and dimming into focus on hover.
Cover Story Hero
A magazine-cover hero composition — the masthead is image-masked and tucked behind the subject, cover lines are arranged in the four corners, the cursor drives parallax, and the cover lines fly off one by one on scroll to make way for the body copy.