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-react
npm install @blakeui/pro-react
yarn add @blakeui/pro-react
bun add @blakeui/pro-react

Usage

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.

PropTypeDefaultDescription
itemsLegendItemData[]The rows to render. Required.
childrenReactElementA single LegendItemComponent cloned for each item — the row template. Required.
hoveredIndexnumber | nullControlled hover index. Passing null is controlled; omitting the prop is uncontrolled.
onHoverChange(index: number | null) => voidCalled when the hovered index changes — pair with a chart's own callback to sync.
titlestringTitle rendered above the list. It does not dim when a row is hovered.
titleClassNamestringClass name for the title.
classNamestringClass name for the root element.
listClassNamestringClass name for the <ul> — where the documented layouts set direction, wrapping, and gap.
interactivebooleantrueWhether rows highlight and join the keyboard roving tabindex. Set false for a descriptive key, which must not be focusable.
aria-labelstringtitle ?? "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.

PropTypeDefaultDescription
classNamestringClass 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.
childrenReactNodeRow content: marker, label, value, progress. Required.

LegendMarker

The colour indicator. A 10px dot by default, painted from the item's resolved series colour.

PropTypeDefaultDescription
classNamestringClass name for the marker — pass a size utility (e.g. size-2) to shrink it.

LegendLabel

The item's label.

PropTypeDefaultDescription
classNamestringClass 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.

PropTypeDefaultDescription
showPercentagebooleanfalseRender the percentage of maxValue beside the value. Ignored on items without a maxValue.
prefixstringSymbol 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.
classNamestringClass name for the value.
percentageClassNamestringClass name for the percentage.

LegendProgress

A progress bar for the item, filled in its own colour. Renders nothing when the item has no maxValue.

PropTypeDefaultDescription
classNamestringClass name for the progress root.
trackClassNamestringClass name for the track.
indicatorClassNamestringClass name for the fill.
heightstringClass name applied to the track, for setting its height alone — e.g. h-1.

LegendItemData

The shape of every entry in items.

FieldTypeDefaultDescription
labelstringDisplay label. Also the row's React key, so it must be unique. Required.
valuenumberCurrent value. Required.
maxValuenumberScale for the progress bar and the percentage. Omit for a plain value row.
colorstringvar(--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>;
}
PropertyTypeDefaultDescription
itemsLegendItemData[]Every item passed to the legend.
hoveredIndexnumber | nullThe currently highlighted index.
setHoveredIndex(index: number | null) => voidSets 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>;
}
PropertyTypeDefaultDescription
itemLegendItemDataThis row's data.
indexnumberThis row's index within items.
isHoveredbooleanWhether this row is the highlighted one.
isFadedbooleanWhether another row is highlighted, so this one is dimmed.
percentagenumbervalue / maxValue * 100, or 0 without a maxValue.
colorstringThe resolved colour — the item's own, or its series-token fallback.

On this page