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.
npx shadcn@latest add https://webberui.com/r/leader-line-captions.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<LeaderLineCaptions />
Installation
npx shadcn@latest add https://webberui.com/r/leader-line-captions.jsonOr, once registries are configured in components.json, install it as @webberui/leader-line-captions.
Usage
image takes any image node (an <img>, a gradient div, whatever), and captions marks detail points in the image with percentage coordinates; the component numbers them and draws the leader lines automatically.
import { LeaderLineCaptions } from "@/components/ui/leader-line-captions";
<LeaderLineCaptions
image={<img src="/product.jpg" alt="Product exterior" />}
side="right"
captions={[
{ x: 30, y: 24, title: "Camera module", description: "Three lenses in a matrix layout." },
{ x: 68, y: 52, title: "Body frame", description: "Aerospace-grade aluminium alloy." },
{ x: 44, y: 82, title: "Speaker grille" },
]}
/>Each caption's x / y is a percentage relative to the image (0–100) and is independent of the image's actual size, so the leader lines realign automatically when it scales.
Controlled focus
Without activeIndex it is uncontrolled, and hovering or focusing a caption focuses it automatically. To keep it in sync with external state, pass activeIndex together with onActiveChange:
const [active, setActive] = React.useState<number | null>(null);
<LeaderLineCaptions
image={<img src="/map.jpg" alt="Map" />}
activeIndex={active}
onActiveChange={setActive}
captions={captions}
/>Props
LeaderLineCaptions
| Prop | Type | Default | Description |
|---|---|---|---|
image | React.ReactNode | — | The image content, filling the image frame edge to edge |
captions | LeaderCaption[] | — | The list of numbered captions, numbered 1…n automatically in order |
side | "left" | "right" | "right" | Which side the caption sidebar sits on |
aspectRatio | string | "4 / 3" | Aspect ratio of the image frame (a CSS aspect-ratio value) |
stagger | number | 0.14 | Interval between leader lines being drawn (seconds) |
delay | number | 0.1 | Delay before the reveal starts (seconds) |
drawDuration | number | 0.5 | Duration of drawing a single leader line (seconds) |
once | boolean | true | Plays the reveal animation only the first time the element enters the viewport |
activeIndex | number | null | — | Controlled focus index; when omitted it is uncontrolled |
defaultActiveIndex | number | null | null | Initial focus index in uncontrolled mode |
onActiveChange | (index: number | null) => void | — | Fired when the focus changes |
dimOpacity | number | 0.55 | How much the unfocused image is dimmed while focused (0–1) |
className | string | — | Custom classes for the outer figure |
LeaderCaption
| Field | Type | Description |
|---|---|---|
x | number | Horizontal position of the detail point (percentage of the image width, 0–100) |
y | number | Vertical position of the detail point (percentage of the image height, 0–100) |
title | React.ReactNode | Caption title |
description | React.ReactNode | Caption body (optional) |
id | string | Custom key / identifier (optional; defaults to the index) |
How it works
- The leader line endpoints are measured live with
getBoundingClientRectagainst the actual positions of the badge and the detail point in the image, and aResizeObserverwatches the container and the sidebar, so everything realigns after a resize, a reflow, or a font load. - On reveal, the leader lines emerge one by one as
pathLengthanimates from 0 to 1, and each number "lights up" in turn once its line is more than halfway drawn. - Hovering or focusing any caption makes a focus halo appear at that detail point while the rest of the image dims under a radial gradient; on mouse out the halo fades away in place rather than snapping back to the origin.
Accessibility
- The caption sidebar is wrapped semantically in
<figure>/<figcaption>, and each caption is a focusable<button>, so keyboardTabmoves through them one by one and triggers the matching focus effect on the image. - The leader line
<svg>, the focus halo, and the small number circles are all decorative only (aria-hidden), so they do not interfere with the screen reader; the caption text itself is readable content. - When the user has "reduce motion" enabled at the system level, the leader lines and numbers are rendered in their final state with no drawing animation, and the focus dimming switches instantly.
Contact Sheet Gallery
A grid gallery laid out like a photographic contact sheet, complete with sprocket-hole film edges and frame numbers; hover circles a frame in grease pencil, and the selected frame enlarges into a work print with a handwritten annotation.
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.