Skip to content

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.

On this page

Example

Loading example…
Dumbbell Chart.tsxtsx
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.

  1. DumbbellChart

    Native figure sharing categorical ranges, labels, and formatting. Owns a DOM element.

  2. ChartTitle

    Native figcaption that visibly names the figure. Owns a DOM element.

  3. DumbbellChartPlot / DumbbellChartDumbbell

    Horizontal SVG ranges with two endpoints and a connecting line. Owns a DOM element.

  4. ChartDescription

    Visible prose summarizing the important range. Owns a DOM element.

  5. ChartTable

    Native table with exact start and end values. Owns a DOM element.

  6. ChartTooltipOptional

    Optional floating range label shown on hover or focus. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start DumbbellChart with labelled start and end values plus visible axis labels.

  2. 2

    Add the supporting parts

    Render each range with DumbbellChartDumbbell so its endpoints share one keyboard-reachable mark.

  3. 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

Importtsx
import { ChartDescription, ChartTable, ChartTitle, ChartTooltip, DumbbellChart, DumbbellChartDumbbell, DumbbellChartPlot } from "@comp0/react";

DumbbellChart

DOM element

Native figure sharing categorical ranges, labels, and formatting.

PropTypeDescription
valuesreadonly DumbbellChartValue[]Category labels with finite start and end values.
categoryLabelstringVisible headings for category and numeric axes.
valueLabelstringVisible headings for category and numeric axes.
startLabelstringAccessible endpoint labels; default to "Start" and "End".
endLabelstringAccessible endpoint labels; default to "Start" and "End".
formatValue(value: number) => stringFormats endpoint ticks and table cells.

ChartTitle

DOM element

Native figcaption that visibly names the figure.

DumbbellChartPlot / DumbbellChartDumbbell

DOM element

Horizontal SVG ranges with two endpoints and a connecting line.

PropTypeDescription
aria-labelstringConcise text alternative naming the comparison.
xMinnumberOptional numeric bounds and tick count.
xMaxnumberOptional numeric bounds and tick count.
xTickCountnumberOptional numeric bounds and tick count.
children(dumbbell: DumbbellChartDumbbellState) => ReactNodeCustom range renderer.
dumbbellDumbbellChartDumbbellStateRange state passed to the mark.

ChartDescription

DOM element

Visible prose summarizing the important range.

ChartTable

DOM element

Native table with exact start and end values.

ChartTooltip

OptionalDOM element

Optional 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 hookMeaning
[data-start]The two endpoint values.
[data-end]The two endpoint values.
[data-active]The range currently reached by pointer or keyboard.

ChartTooltip

Style hookMeaning
[data-open]A value tooltip is visible.

Keep exploring