Legend
A composable legend — a list whose row template you author once, with markers, rolling values, progress bars, and a hover contract that syncs both ways with a chart.
Sessions by channel
- Organic4,25085%%
- Paid3,12062%%
- Email2,10042%%
- Social1,58032%%
Installation
pnpm add @blakeui/pro-reactnpm install @blakeui/pro-reactyarn add @blakeui/pro-reactbun add @blakeui/pro-reactUsage
Legend is composable, unlike the charts. A chart is one component with props because compositional
children would fragment its rendering pipeline; a legend is a layout primitive — a list whose row
template changes per use. Props cannot express a three-column row with a progress bar spanning its
full width, so the row is authored as children and repeated for every item.
You write the template once. Legend supplies the data and the hover state; the parts read them
from context.
import {
Legend,
LegendItemComponent,
LegendLabel,
LegendMarker,
LegendValue,
} from "@blakeui/pro-react";
const channels = [
{label: "Organic", maxValue: 5000, value: 4250},
{label: "Paid", maxValue: 5000, value: 3120},
];
<Legend items={channels} title="Sessions by channel">
<LegendItemComponent className="flex items-center gap-3">
<LegendMarker />
<LegendLabel className="w-full min-w-0 flex-1" />
<LegendValue />
</LegendItemComponent>
</Legend>;Anatomy
Legend renders a real list — a root holding the optional title and a <ul>, with one <li> per
item. className styles the root; listClassName styles the <ul>, which is the box the
documented layouts change, and LegendItemComponent's className styles the row itself.
Colours
Markers default to the categorical series tokens by position — --chart-1 through --chart-5,
cycling — which is the same mapping the charts use. A legend beside a chart therefore matches it
without any colour wiring. Pass color on an item only to override a specific row.
maxValue is the switch
An item with maxValue is a progress row: it has a percentage and LegendProgress renders a bar.
An item without one is a plain value row — the percentage is suppressed and LegendProgress renders
nothing at all. That is why the same template works for both kinds of data.
The percentage label rounds (31.6% → 32%) while the bar fills to the exact fraction, so at a
rounding boundary the two disagree by under a percent. The label is for reading; the bar is for
comparing.
Numbers roll
Values and percentages render through the shared odometer, so a legend driven by live data animates
between figures instead of snapping. The odometer is decorative and hidden from assistive tech, and
each number is paired with a visually-hidden text node carrying the real string — which is what
formatValue and formatPercentage shape. They format the accessible text; they cannot turn the
roll off. For a leading symbol use prefix.
Motion and accessibility
Hovering a row lifts it onto a background pill and dims the others to 40% over 150ms. The title does not dim — it is not one of the rows.
An interactive legend is one tab stop. Arrow keys move the highlight between rows, Home and End jump
to the ends, Escape clears it, and a polite live region announces the active row. Set
interactive={false} for a purely descriptive key: it drops the handlers and the tab stop, because
a static item must not be focusable.
Simple
Marker, label, value. With no maxValue on the data there is no bar and no percentage — the same
template, less data.
- North America18,420
- Europe12,960
- Asia Pacific9,340
- Latin America4,180
Horizontal
Turn the list into a wrapping row through listClassName, and shrink the marker and label to match.
This is the shape that sits under a chart that already shows its own values.
- Starter
- Growth
- Scale
- Enterprise
- Trial
Synced with a chart
hoveredIndex and onHoverChange are the same controlled pair the charts expose. Drive both off one
piece of state and hover syncs in both directions — hovering a ring highlights its row, hovering a
row pops its ring. Keyboard navigation writes the same state, so it drives the chart too.
Passing hoveredIndex makes the legend controlled; null is a controlled "nothing hovered", while
omitting the prop entirely leaves the legend to manage its own state.
Storage
- Documents312
- Media268
- Backups194
API Reference
Legend
The root. Holds the data and the hover contract, and repeats the row template for every item.
| Prop | Type | Default | Description |
|---|---|---|---|
items | LegendItemData[] | — | The rows to render. Required. |
children | ReactElement | — | A single LegendItemComponent cloned for each item — the row template. Required. |
hoveredIndex | number | null | — | Controlled hover index. Passing null is controlled; omitting the prop is uncontrolled. |
onHoverChange | (index: number | null) => void | — | Called when the hovered index changes — pair with a chart's own callback to sync. |
title | string | — | Title rendered above the list. It does not dim when a row is hovered. |
titleClassName | string | — | Class name for the title. |
className | string | — | Class name for the root element. |
listClassName | string | — | Class name for the <ul> — where the documented layouts set direction, wrapping, and gap. |
interactive | boolean | true | Whether rows highlight and join the keyboard roving tabindex. Set false for a descriptive key, which must not be focusable. |
aria-label | string | title ?? "Legend" | Accessible name for the list. |
LegendItemComponent
The row template. Its className is where the row's layout lives; hover and fade state are applied
by the list item around it.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Class name for the row — e.g. flex items-center gap-3 or grid grid-cols-[auto_1fr_auto] items-center gap-x-3 gap-y-1. |
children | ReactNode | — | Row content: marker, label, value, progress. Required. |
LegendMarker
The colour indicator. A 10px dot by default, painted from the item's resolved series colour.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Class name for the marker — pass a size utility (e.g. size-2) to shrink it. |
LegendLabel
The item's label.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Class name for the label. Use w-full min-w-0 flex-1 to let it take the row's slack without collapsing on narrow widths. |
LegendValue
The item's value, optionally with its percentage. Both roll through the shared odometer.
| Prop | Type | Default | Description |
|---|---|---|---|
showPercentage | boolean | false | Render the percentage of maxValue beside the value. Ignored on items without a maxValue. |
prefix | string | — | Symbol rendered ahead of the rolling digits, e.g. "$". |
formatValue | (value: number) => string | (v) => v.toLocaleString() | Accessible text for the value. The visible number always rolls — this shapes what assistive tech reads, not whether it animates. |
formatPercentage | (percentage: number) => string | (p) => `${p.toFixed(0)}%` | Accessible text for the percentage, on the same terms. |
className | string | — | Class name for the value. |
percentageClassName | string | — | Class name for the percentage. |
LegendProgress
A progress bar for the item, filled in its own colour. Renders nothing when the item has no
maxValue.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Class name for the progress root. |
trackClassName | string | — | Class name for the track. |
indicatorClassName | string | — | Class name for the fill. |
height | string | — | Class name applied to the track, for setting its height alone — e.g. h-1. |
LegendItemData
The shape of every entry in items.
| Field | Type | Default | Description |
|---|---|---|---|
label | string | — | Display label. Also the row's React key, so it must be unique. Required. |
value | number | — | Current value. Required. |
maxValue | number | — | Scale for the progress bar and the percentage. Omit for a plain value row. |
color | string | var(--chart-n) | Marker and fill colour. Defaults to the series token for the item's position. |
useLegend
Reads the legend context from any descendant — the items and the shared hover state.
import {useLegend} from "@blakeui/pro-react";
function RowCount() {
const {items, hoveredIndex, setHoveredIndex} = useLegend();
return <span>{items.length} channels</span>;
}| Property | Type | Default | Description |
|---|---|---|---|
items | LegendItemData[] | — | Every item passed to the legend. |
hoveredIndex | number | null | — | The currently highlighted index. |
setHoveredIndex | (index: number | null) => void | — | Sets the highlight, honouring the controlled/uncontrolled contract. |
useLegendItem
Reads the current row's data and derived state. Only valid inside a LegendItemComponent — use it
to build a custom part alongside the built-in ones.
import {useLegendItem} from "@blakeui/pro-react";
function Delta() {
const {item, isHovered, percentage} = useLegendItem();
return <span>{percentage.toFixed(1)}%</span>;
}| Property | Type | Default | Description |
|---|---|---|---|
item | LegendItemData | — | This row's data. |
index | number | — | This row's index within items. |
isHovered | boolean | — | Whether this row is the highlighted one. |
isFaded | boolean | — | Whether another row is highlighted, so this one is dimmed. |
percentage | number | — | value / maxValue * 100, or 0 without a maxValue. |
color | string | — | The resolved colour — the item's own, or its series-token fallback. |
Chart Chrome
The layers a chart draws around its data — a tinted plot surface, gridlines behind the plot, a shaded region marking a range, and the dashed tail that marks a forecast apart from a measurement.
Tooltip
The panel every chart shows on hover — composed from its parts, or driven straight from a payload by the adapter the charts use internally.