Margin Note

Annotated Line Chart for React

A line chart that reads itself. A reader sweeps across the series and inks it as it goes; wherever something happened it stops, marks the point and writes a note in the margin, level with the point and tied to it by a leader. Hover a note and its stretch of the chart lights up.

  • Chart
  • Data
  • AI

Uses motion, clsx, tailwind-merge

Open in ChatGPTOpen in Claude
duration2400ms
hold280ms(replays the demo)
<MarginNote />

replays the demo when you let go.

Kiln · Weekly signups

15,859last 16 weeks

Signups are up 38% since the free plan went to three seats.

16 points, Jul 7 to Oct 20, from 498 to 1,720. Note 1, at Jul 28: Launch week (+163%). 1,720 signups, 2.6× a normal week. Note 2, at Sep 1: Login outage (−42%). Sign-in was down for six hours on Tuesday. Note 3, at Oct 13: Free plan, three seats (+38%). Signups held 38% higher for four weeks.

Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Margin Note: A line chart that reads itself. A reader sweeps across the series and inks it as it goes; wherever something happened it stops, marks the point and writes a note in the margin, level with the point and tied to it by a leader. Hover a note and its stretch of the chart lights up.
This demo, Weekly Signups: Sixteen weeks of signups for a design tool, read with notes on launch week, a login outage and the free plan going to three seats.

1. Install it:
   npx shadcn@latest add @keyframeui/margin-note-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/margin-note-demo.json instead.)
   It adds the demo, components/margin-note-demo.tsx, and the component, components/ui/margin-note.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <MarginNote> on its own.

3. These props set its feel. Change them before you change the file:
   - duration (default 2400): How long the read takes, in ms, pauses at the notes included.
   - hold (default 280): How long the reader stops at each noted point, in ms, as if writing. The pauses never take more than 45% of `duration`.

4. Don't change, in components/ui/margin-note.tsx:
   - The read is one motion `animate` whose `onUpdate` writes the clip, the glow and the reader's transforms straight to the DOM; React re-renders only when the reader passes a noted point.
   - The reader holds at each noted point, the holds never take more than `holdShare` of `duration`, and between them it sweeps at one steady, linear pace.
   - Every note is rendered from the start, so the margin holds its room, and each is placed level with its point once measured; nothing shifts when the notes arrive.
   - Inputs are keyed by content (`dataKey`, `notesKey`), so a re-render with equal inline arrays doesn't restart the read; new data or notes read again from the start.
   - Under reduced motion or `play={false}` nothing sweeps: the chart arrives read with every note in place, and highlights and the crosshair still work.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/margin-note.md

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/margin-note.tsx and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/margin-note

Examples

Finished sections built with Margin Note. Each one installs whole, with the component included.

Incident Read

A day of p95 latency on an API. The reader stops at the morning peak, the deploy that sent it to 842 ms and the rollback that brought it down.

api.harbor.dev / p95 latency / today

One deploy cost two slow hours at checkout. Rolling it back fixed it.

24 points, 00:00 to 23:00, from 169 ms to 842 ms. Note 1, at 09:00: Morning peak (238 ms). EU traffic tops out; p95 stays under 240 ms. Note 2, at 14:00: Deploy 4812 (+281%). An N+1 query in /orders sent p95 to 842 ms. Note 3, at 16:00: Rolled back to 4811 (−75%). Under 210 ms again by 16:00.

Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Margin Note: A line chart that reads itself. A reader sweeps across the series and inks it as it goes; wherever something happened it stops, marks the point and writes a note in the margin, level with the point and tied to it by a leader. Hover a note and its stretch of the chart lights up.
This demo, Incident Read: A day of p95 latency on an API. The reader stops at the morning peak, the deploy that sent it to 842 ms and the rollback that brought it down.

1. Install it:
   npx shadcn@latest add @keyframeui/margin-note-latency-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/margin-note-latency-demo.json instead.)
   It adds the demo, components/margin-note-latency-demo.tsx, and the component, components/ui/margin-note.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <MarginNote> on its own.

3. These props set its feel. Change them before you change the file:
   - duration (default 2400): How long the read takes, in ms, pauses at the notes included.
   - hold (default 280): How long the reader stops at each noted point, in ms, as if writing. The pauses never take more than 45% of `duration`.

4. Don't change, in components/ui/margin-note.tsx:
   - The read is one motion `animate` whose `onUpdate` writes the clip, the glow and the reader's transforms straight to the DOM; React re-renders only when the reader passes a noted point.
   - The reader holds at each noted point, the holds never take more than `holdShare` of `duration`, and between them it sweeps at one steady, linear pace.
   - Every note is rendered from the start, so the margin holds its room, and each is placed level with its point once measured; nothing shifts when the notes arrive.
   - Inputs are keyed by content (`dataKey`, `notesKey`), so a re-render with equal inline arrays doesn't restart the read; new data or notes read again from the start.
   - Under reduced motion or `play={false}` nothing sweeps: the chart arrives read with every note in place, and highlights and the crosshair still work.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/margin-note.md

Year Against Year

Monthly revenue against last year's, dashed underneath, with notes on annual plans, a team tier and a record December.

Tinta · Revenue by month

$940k+64% on 2024

20252024

2025 closed 64% ahead of 2024, most of it after the team tier.

12 points, Jan to Dec, from $42k to $131k. Note 1, at Mar: Annual plans (+29%). Two months free on yearly billing. Note 2, at Aug: Team tier (+25%). Studios moved up; the average order rose to $212. Note 3, at Dec: Gift cards (+87%). The best December yet, 1.9× last year's.

Take this demo to your coding agent.
Add this moment from KeyframeUI to our project and adapt it to our content.

Margin Note: A line chart that reads itself. A reader sweeps across the series and inks it as it goes; wherever something happened it stops, marks the point and writes a note in the margin, level with the point and tied to it by a leader. Hover a note and its stretch of the chart lights up.
This demo, Year Against Year: Monthly revenue against last year's, dashed underneath, with notes on annual plans, a team tier and a record December.

1. Install it:
   npx shadcn@latest add @keyframeui/margin-note-revenue-demo
   (If the CLI can't find @keyframeui, use https://keyframeui.com/r/margin-note-revenue-demo.json instead.)
   It adds the demo, components/margin-note-revenue-demo.tsx, and the component, components/ui/margin-note.tsx.

2. Make it ours: our copy, data, images and colors. Keep the structure the motion depends on. If we only need the effect, use <MarginNote> on its own.

3. These props set its feel. Change them before you change the file:
   - duration (default 2400): How long the read takes, in ms, pauses at the notes included.
   - hold (default 280): How long the reader stops at each noted point, in ms, as if writing. The pauses never take more than 45% of `duration`.

4. Don't change, in components/ui/margin-note.tsx:
   - The read is one motion `animate` whose `onUpdate` writes the clip, the glow and the reader's transforms straight to the DOM; React re-renders only when the reader passes a noted point.
   - The reader holds at each noted point, the holds never take more than `holdShare` of `duration`, and between them it sweeps at one steady, linear pace.
   - Every note is rendered from the start, so the margin holds its room, and each is placed level with its point once measured; nothing shifts when the notes arrive.
   - Inputs are keyed by content (`dataKey`, `notesKey`), so a re-render with equal inline arrays doesn't restart the read; new data or notes read again from the start.
   - Under reduced motion or `play={false}` nothing sweeps: the chart arrives read with every note in place, and highlights and the crosshair still work.
   Its timing is all in the TIMING block at the top of that file.

Docs: https://keyframeui.com/components/margin-note.md

Props

data Required

Typenumber[]

The series, one value per point, oldest first.

labels

Typestring[]

A label per point, like a date. A few run under the chart, and the crosshair shows the one it's on.

notes

TypeMarginNoteNote[]Default[]

What the reader writes in the margin: { at, to?, title, body?, delta? }. at is an index in data; give a to for a stretch of time and its band shades, with the note written at the stretch's end. They're numbered in reading order.

summary

TypeReact.ReactNode

The takeaway above the chart, written once the read ends. Plain text rises in word by word.

format

Type(value: number) => string

Formats values on the axis and under the crosshair. Defaults to compact numbers (1.2K).

compare

Typenumber[]

A second series to compare against, like last period. Drawn dashed and never read.

play

Typeboolean | "view"Default"view"

"view" reads once the chart is 35% in view. true reads now, and again each time it flips back to true. false shows it already read.

duration

TypenumberDefault2400

How long the read takes, in ms, pauses at the notes included.

hold

TypenumberDefault280

How long the reader stops at each noted point, in ms, as if writing. The pauses never take more than 45% of duration.

label

TypestringDefault"Chart"

What the chart shows, like "Weekly signups". Screen readers hear it first.

onComplete

Type() => void

Called when a read ends.

className

Typestring

Classes for the root. Set --margin-note-accent for the reader, points, bands and numbers, and --margin-note-height for the plot's height (12.5rem narrow, 15rem wide). Ink comes from the text color.