WebberUI

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.

How to install Pro components →See the plans →

Loading preview…
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.

640
<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

PropTypeDefaultDescription
childrenReact.ReactNodeThe article content, with Sidenote placed inside the body text
sidenoteWidthstring"12rem"Width of the margin column (any CSS length)
gapstring"2.5rem"Gap between the main column and the margin column
breakpointnumber640Below this available article width (px), it falls back to inline expandable notes
containerReact.RefObject<HTMLElement | null>Ref of the nested scroll container; defaults to the window as the scroll root
classNamestringAppended to the outermost container

Sidenote

PropTypeDefaultDescription
childrenReact.ReactNodeThe marked phrase in the body text, highlighted when it becomes the focus
noteReact.ReactNodeThe sidenote content: shown in the margin column on wide screens, expanded inline on narrow ones
idstringautomaticAnchor id; generated automatically when omitted, and must be stable for correct pairing
labelReact.ReactNodeautomatic numberOverrides the superscript marker (for example instead of a number)
classNamestringAppended 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 IntersectionObserver tracks 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 via aria-describedby, and on narrow screens aria-expanded / aria-controls describe 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.

On this page