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 className="sr-only">Delivery-time ranges</caption>
<thead>
<tr>
<th scope="col">Service</th>
<th scope="col">Start</th>
<th scope="col">End</th>
</tr>
</thead>
<tbody>
{delivery.map((item) => (
<tr key={item.label}>
<th scope="row">{item.label}</th>
<td>{formatHours(item.start)}</td>
<td>{formatHours(item.end)}</td>
</tr>
))}
</tbody>
</ChartTable>
<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 with exact start and end values. 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> <caption>Delivery ranges</caption> </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 with exact start and end values.
ChartTooltip
OptionalDOM elementOptional floating range label shown on hover or focus.
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. |