# Marginalia — Animated UI Component for React

> Margin notes for prose — inline marks get a hand-drawn pen underline that draws in on view; hover, focus or click reveals each note in a gutter rail aligned to its line, collision-stacked and joined to the word by a hairline leader that draws out to it. Notes take any content (images, charts), fold into inline expanding blocks on narrow containers, and preview once in a ghost reading pass. Marks are real buttons with aria-expanded/aria-controls and Escape to close.

- Page: https://crucibleui.com/components/utility/marginalia
- Category: [utility](https://crucibleui.com/components/utility)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/marginalia
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/marginalia`.

## Props (`MarginaliaProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | The prose. Each direct child is treated as one block (paragraph, list, quote…) — on narrow containers a mark's note expands inline directly beneath the block that contains it. Place `<Mark>` elements anywhere inside those blocks. |
| `rail?` | `"right" \| "left"` | `"right"` | Which page gutter the notes stack in on wide containers. |
| `accentColor?` | `string` | `"#a5b4fc"` | Ink color for underlines, leader lines and note numerals. |
| `railWidth?` | `number` | `224` | Width of the note rail in px on wide containers. |
| `gutter?` | `number` | `40` | Gap between the prose column and the rail in px. |
| `breakpoint?` | `number` | `640` | Container width (px) below which the rail collapses and notes open as inline expanding blocks beneath their paragraph instead. |
| `ghost?` | `boolean` | `true` | On first view, preview each note in reading order once (a ghost reading pass), then settle closed. Cancelled by any real interaction; skipped under reduced motion and on narrow containers. |
| `className?` | `string` | — | Classes merged onto the root element. |
| `ref?` | `React.Ref<MarginaliaHandle>` | — | Imperative handle — see {@link MarginaliaHandle}. |

## Usage example

```tsx
import { Marginalia, Mark } from "@/components/crucible/marginalia";

/** A small procedural plate: dusk sky, low sun, a sandbar linking an island to shore. */
function SandbarPlate() {
  return (
    <svg viewBox="0 0 200 92" className="mb-2 block h-auto w-full rounded-md" role="img" aria-label="Illustration of a sandbar joining an island to the shore at low tide">
      <defs>
        <linearGradient id="mg-sky" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="#1e2a4a" />
          <stop offset="60%" stopColor="#5b6aa0" />
          <stop offset="100%" stopColor="#c9b8d8" />
        </linearGradient>
        <linearGradient id="mg-sea" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="#3b4a78" />
          <stop offset="100%" stopColor="#141a30" />
        </linearGradient>
        <radialGradient id="mg-sun" cx="0.5" cy="0.5" r="0.5">
          <stop offset="0%" stopColor="#fff6e6" />
          <stop offset="100%" stopColor="#fff6e6" stopOpacity="0" />
        </radialGradient>
      </defs>
      <rect width="200" height="56" fill="url(#mg-sky)" />
      <circle cx="132" cy="48" r="20" fill="url(#mg-sun)" opacity="0.7" />
      <circle cx="132" cy="49" r="6" fill="#fff4e0" />
      <rect y="54" width="200" height="38" fill="url(#mg-sea)" />
      <path d="M118 56 C124 42 142 40 154 47 C160 50 166 54 170 56 Z" fill="#1a2140" />
      <path d="M0 70 C40 64 70 62 110 60 C130 58 150 57 165 56 L170 58 C150 60 128 62 112 64 C76 68 40 72 0 78 Z" fill="#cdb9a0" opacity="0.85" />
      <path d="M0 78 C40 72 76 68 112 64 L0 92 Z" fill="#a8927a" opacity="0.5" />
      <path d="M128 66 L176 66 M140 72 L190 72 M150 80 L196 80" stroke="#fff4e0" strokeOpacity="0.25" strokeWidth="0.8" />
    </svg>
  );
}

/** A tiny tide-height chart for a single day (two highs, two lows). */
function TideChart() {
  const heights = [2.1, 3.4, 4.6, 5.0, 4.2, 2.8, 1.5, 0.9, 1.6, 3.0, 4.4, 4.9, 4.1, 2.6, 1.3, 0.8];
  return (
    <div className="mb-2">
      <svg viewBox="0 0 160 46" className="block h-auto w-full" role="img" aria-label="Bar chart of tide height over one day showing two highs and two lows">
        {heights.map((h, i) => (
          <rect
            key={i}
            x={i * 10 + 1}
            y={44 - h * 8}
            width={7}
            height={h * 8}
            rx={1.5}
            fill="#a5b4fc"
            fillOpacity={h > 4.5 ? 0.95 : 0.28}
          />
        ))}
        <line x1="0" y1="44.5" x2="160" y2="44.5" stroke="white" strokeOpacity="0.15" />
      </svg>
      <div className="mt-1 flex justify-between font-mono text-[9.5px] text-neutral-500">
        <span>00:00</span>
        <span>12:00</span>
        <span>24:00</span>
      </div>
    </div>
  );
}

export default function MarginaliaDemo() {
  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950 px-6 py-10 sm:px-10">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_30%_20%,rgba(255,255,255,0.045),transparent_65%)]"
      />
      <div className="relative z-10 w-full max-w-[820px]">
        <p className="mb-5 font-mono text-[10.5px] uppercase tracking-[0.2em] text-neutral-500">
          Hover or tab to a marked word.
        </p>
        <Marginalia className="text-[15px] leading-[1.8] text-neutral-400 [&_p]:mb-4 [&_p:last-child]:mb-0">
          <p>
            <span className="text-neutral-200">Twice a day the sea leaves.</span> It does not go far,
            and it always comes back, but for a few hours the shoreline belongs to whoever walks
            out onto it. The rhythm is set by{" "}
            <Mark note={<><TideChart />Two highs and two lows roughly every 24 hours and 50 minutes — the Moon rises a little later each day.</>}>
              the Moon&apos;s pull
            </Mark>
            , a slow tug that drags the water into a long bulge.
          </p>
          <p>
            Twice a month, when Sun and Moon line up, the range widens into{" "}
            <Mark note="Nothing to do with the season — the name comes from the water “springing” higher and lower than usual.">
              spring tides
            </Mark>
            . The low water uncovers things the sea usually keeps: ribbed sand, old posts, a
            path of shingle running out to an island called a{" "}
            <Mark note={<><SandbarPlate />A sand or shingle bar that ties an island to the mainland, walkable only near low water.</>}>
              tombolo
            </Mark>
            .
          </p>
          <p>
            Between the turns there is a pause. The current stops, the surface flattens, and
            for a few minutes nothing moves at all. Sailors call it{" "}
            <Mark note="The brief still moment when the tide turns and the current slackens to nothing.">
              slack water
            </Mark>
            , and it is the best time to cross.
          </p>
        </Marginalia>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.
