Charts
Dumbbell Chart
A categorical comparison that shows the start and end of every range on one line.
When to use it: Use it to compare before-and-after values or ranges across a small set of categories.
Example
import {
ChartDescription,
ChartTable,
ChartTitle,
ChartTooltip,
DumbbellChart,
DumbbellChartDumbbell,
DumbbellChartPlot,
} from "@comp0/react";
const delivery = [
{ label: "Standard", start: 3, end: 8 },
{ label: "Express", start: 5, end: 11 },
{ label: "Pickup", start: 2, end: 6 },
] as const;
const formatHours = (value: number) => `${value}h`;
export function Example() {
return (
<DumbbellChart
values={delivery}
categoryLabel="Service"
valueLabel="Hours"
formatValue={formatHours}
className="w-full max-w-2xl rounded-lg has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-4 has-[:focus-visible]:outline-teal-600 dark:has-[:focus-visible]:outline-teal-400"
>
<ChartTitle className="text-base font-semibold text-zinc-950 dark:text-zinc-50">
Delivery-time range
</ChartTitle>
<DumbbellChartPlot
aria-label="Dumbbell chart comparing delivery time ranges"
className="mx-auto mt-5 aspect-square w-full max-w-md overflow-visible"
>
{(dumbbell) => (
<DumbbellChartDumbbell dumbbell={dumbbell} className="group outline-none">
<line
x1={dumbbell.startX}
x2={dumbbell.endX}
y1={dumbbell.y}
y2={dumbbell.y}
className="stroke-teal-600 group-data-active:stroke-zinc-950 dark:stroke-teal-400 dark:group-data-active:stroke-white"
strokeWidth="3"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
<circle
cx={dumbbell.startX}
cy={dumbbell.y}
r="2.6"
className="fill-white stroke-teal-700 dark:fill-zinc-950 dark:stroke-teal-300"
strokeWidth="1.5"
vectorEffect="non-scaling-stroke"
/>
<circle
cx={dumbbell.endX}
cy={dumbbell.y}
r="2.6"
className="fill-teal-700 stroke-white dark:fill-teal-300 dark:stroke-zinc-950"
strokeWidth="1.5"
vectorEffect="non-scaling-stroke"
/>
</DumbbellChartDumbbell>
)}
</DumbbellChartPlot>
<ChartDescription className="mt-5 text-sm text-zinc-600 dark:text-zinc-400">
Express delivery spans the widest range of promised hours.
</ChartDescription>
<ChartTable className="mt-4 w-full border-collapse text-left text-sm [&_td]:border-t [&_td]:border-zinc-200 [&_td]:py-2 [&_th]:border-zinc-200 [&_th]:py-2 dark:[&_td]:border-zinc-800 dark:[&_th]:border-zinc-800 [&_caption]:sr-only" />
<ChartTooltip className="pointer-events-none z-50 rounded-md bg-zinc-950 px-2 py-1 text-sm text-white shadow-lg dark:bg-zinc-50 dark:text-zinc-950" />
</DumbbellChart>
);
}Anatomy
Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.
A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.
DumbbellChart
Native figure sharing categorical ranges, labels, and formatting. Owns a DOM element.
ChartTitle
Native figcaption that visibly names the figure. Owns a DOM element.
DumbbellChartPlot / DumbbellChartDumbbell
Horizontal SVG ranges with two endpoints and a connecting line. Owns a DOM element.
ChartDescription
Visible prose summarizing the important range. Owns a DOM element.
ChartTable
Native table that renders a complete default from the chart data: a caption from the ChartTitle text or axis labels, column headers, and one row per value. Pass children for a custom body. Owns a DOM element.
ChartTooltipOptional
Optional floating range label shown on hover or focus. Owns a DOM element.
Step by step
- 1
Add the main part
Start DumbbellChart with labelled start and end values plus visible axis labels.
- 2
Add the supporting parts
Render each range with DumbbellChartDumbbell so its endpoints share one keyboard-reachable mark.
- 3
Make the behavior clear
Add a description and native table with both endpoints; use shape and position as well as color to show direction.
Exampletsx <DumbbellChart values={delivery} categoryLabel="Service" valueLabel="Hours"> <ChartTitle>Delivery-time range</ChartTitle> <DumbbellChartPlot aria-label="Dumbbell chart comparing delivery time ranges"> {(dumbbell) => ( <DumbbellChartDumbbell dumbbell={dumbbell}> <line x1={dumbbell.startX} x2={dumbbell.endX} y1={dumbbell.y} y2={dumbbell.y} /> </DumbbellChartDumbbell> )} </DumbbellChartPlot> <ChartTable /> <ChartTooltip /> </DumbbellChart>;
Keyboard
- ⇥
- Enters the chart at its current range and leaves with one more Tab.
- ↓
- Moves to the next range without wrapping.
- ↑
- Moves to the previous range without wrapping.
- Home
- Moves to the first range.
- End
- Moves to the last range.
- Esc
- Dismisses an open ChartTooltip.
Forms and accessibility
Charts are descriptive content and do not create form values.
Accessibility checklist
- Keep category and value axis labels visible and format both endpoints with the same units as the table.
- Wrap each range in DumbbellChartDumbbell so its start and end values share one roving tab stop.
- Announce both endpoints in the mark name; use endpoint shapes or labels rather than color alone for direction.
- Keep ChartTooltip optional and include both exact endpoints in a native table.
API reference
import { ChartDescription, ChartTable, ChartTitle, ChartTooltip, DumbbellChart, DumbbellChartDumbbell, DumbbellChartPlot } from "@comp0/react";DumbbellChart
DOM elementNative figure sharing categorical ranges, labels, and formatting.
| Prop | Type | Description |
|---|---|---|
values | readonly DumbbellChartValue[] | Category labels with finite start and end values. |
categoryLabel | string | Visible headings for category and numeric axes. |
valueLabel | string | Visible headings for category and numeric axes. |
startLabel | string | Accessible endpoint labels; default to "Start" and "End". |
endLabel | string | Accessible endpoint labels; default to "Start" and "End". |
formatValue | (value: number) => string | Formats endpoint ticks and table cells. |
ChartTitle
DOM elementNative figcaption that visibly names the figure.
DumbbellChartPlot / DumbbellChartDumbbell
DOM elementHorizontal SVG ranges with two endpoints and a connecting line.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Concise text alternative naming the comparison. |
xMin | number | Optional numeric bounds and tick count. |
xMax | number | Optional numeric bounds and tick count. |
xTickCount | number | Optional numeric bounds and tick count. |
children | (dumbbell: DumbbellChartDumbbellState) => ReactNode | Custom range renderer. |
dumbbell | DumbbellChartDumbbellState | Range state passed to the mark. |
ChartDescription
DOM elementVisible prose summarizing the important range.
ChartTable
DOM elementNative table that renders a complete default from the chart data: a caption from the ChartTitle text or axis labels, column headers, and one row per value. Pass children for a custom body.
ChartTooltip
OptionalDOM elementOptional floating range label shown on hover or focus.
| Prop | Type | Description |
|---|---|---|
placement | PopoverPlacement | Side of the active mark; defaults to "top". |
offset | number | Distance from the active mark; defaults to eight pixels. |
children | ReactNode | (details: ChartValueDetails) => ReactNode | Custom content receiving the active dumbbell's formatted details. |
Style hooks
Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.
DumbbellChartPlot / DumbbellChartDumbbell
| Style hook | Meaning |
|---|---|
[data-start] | The two endpoint values. |
[data-end] | The two endpoint values. |
[data-active] | The range currently reached by pointer or keyboard. |
ChartTooltip
| Style hook | Meaning |
|---|---|
[data-open] | A value tooltip is visible. |