Skip to content

Charts

Lollipop Chart

A categorical comparison that pairs a thin value stem with a prominent endpoint.

When to use it: Use it for ranked comparisons where bars feel too heavy and the endpoint deserves emphasis.

On this page

Example

Loading example…
Lollipop Chart.tsxtsx
import {
  ChartDescription,
  ChartTable,
  ChartTitle,
  ChartTooltip,
  LollipopChart,
  LollipopChartLollipop,
  LollipopChartPlot,
} from "@comp0/react";

const adoption = [
  { label: "Email", value: 82 },
  { label: "Calendar", value: 64 },
  { label: "Files", value: 48 },
  { label: "Chat", value: 35 },
] as const;

const formatPercent = (value: number) => `${value}%`;

export function Example() {
  return (
    <LollipopChart
      values={adoption}
      categoryLabel="Feature"
      valueLabel="Adoption"
      formatValue={formatPercent}
      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">
        Feature adoption
      </ChartTitle>
      <LollipopChartPlot
        aria-label="Lollipop chart comparing feature adoption"
        xMin={0}
        xMax={100}
        className="mx-auto mt-5 aspect-square w-full max-w-md overflow-visible"
      >
        {(lollipop) => (
          <LollipopChartLollipop lollipop={lollipop} className="group outline-none">
            <line
              x1={lollipop.baseline}
              x2={lollipop.x}
              y1={lollipop.y}
              y2={lollipop.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={lollipop.x}
              cy={lollipop.y}
              r="3"
              className="fill-teal-700 stroke-white dark:fill-teal-300 dark:stroke-zinc-950"
              strokeWidth="1.5"
              vectorEffect="non-scaling-stroke"
            />
          </LollipopChartLollipop>
        )}
      </LollipopChartPlot>
      <ChartDescription className="mt-5 text-sm text-zinc-600 dark:text-zinc-400">
        Email is the most adopted feature, while chat has the most room to grow.
      </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">Feature adoption percentages</caption>
        <thead>
          <tr>
            <th scope="col">Feature</th>
            <th scope="col">Adoption</th>
          </tr>
        </thead>
        <tbody>
          {adoption.map((item) => (
            <tr key={item.label}>
              <th scope="row">{item.label}</th>
              <td>{formatPercent(item.value)}</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" />
    </LollipopChart>
  );
}

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

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

  2. ChartTitle

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

  3. LollipopChartPlot / LollipopChartLollipop

    Horizontal stems and endpoint circles against visible axes. Owns a DOM element.

  4. ChartDescription

    Visible prose summarizing the comparison. Owns a DOM element.

  5. ChartTable

    Native table with exact category values. Owns a DOM element.

  6. ChartTooltipOptional

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

Step by step

  1. 1

    Add the main part

    Start LollipopChart with labelled values and visible category and value axis labels.

  2. 2

    Add the supporting parts

    Render each endpoint with LollipopChartLollipop so the value and category share one keyboard-reachable mark.

  3. 3

    Make the behavior clear

    Keep the baseline and category labels visible, and include the exact values in a native table.

    Exampletsx
    <LollipopChart values={adoption} categoryLabel="Feature" valueLabel="Adoption">
      <ChartTitle>Feature adoption</ChartTitle>
      <LollipopChartPlot aria-label="Lollipop chart comparing feature adoption">
        {(lollipop) => (
          <LollipopChartLollipop lollipop={lollipop}>
            <circle cx={lollipop.x} cy={lollipop.y} r="2" />
          </LollipopChartLollipop>
        )}
      </LollipopChartPlot>
      <ChartTable>
        <caption>Feature adoption</caption>
      </ChartTable>
      <ChartTooltip />
    </LollipopChart>;

Keyboard

Enters the chart at its current lollipop and leaves with one more Tab.
Moves to the next lollipop without wrapping.
Moves to the previous lollipop without wrapping.
Home
Moves to the first lollipop.
End
Moves to the last lollipop.
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 ticks with the same units as the table.
  • Wrap each endpoint in LollipopChartLollipop so its category and value are keyboard reachable.
  • Keep the stem and endpoint visible in every color scheme; do not encode ranking through color alone.
  • Include exact values in a native table and use ChartTooltip only as an enhancement.

API reference

Importtsx
import { ChartDescription, ChartTable, ChartTitle, ChartTooltip, LollipopChart, LollipopChartLollipop, LollipopChartPlot } from "@comp0/react";

LollipopChart

DOM element

Native figure sharing categorical values, labels, and formatting.

PropTypeDescription
valuesreadonly CategoricalChartValue[]Category labels and finite numeric values.
categoryLabelstringVisible headings for category and numeric axes.
valueLabelstringVisible headings for category and numeric axes.
formatValue(value: number) => stringFormats numeric ticks and table cells.

ChartTitle

DOM element

Native figcaption that visibly names the figure.

LollipopChartPlot / LollipopChartLollipop

DOM element

Horizontal stems and endpoint circles against visible axes.

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(lollipop: LollipopChartLollipopState) => ReactNodeCustom endpoint renderer.
lollipopLollipopChartLollipopStateEndpoint state passed to the mark.

ChartDescription

DOM element

Visible prose summarizing the comparison.

ChartTable

DOM element

Native table with exact category values.

ChartTooltip

OptionalDOM element

Optional floating value 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.

LollipopChartPlot / LollipopChartLollipop

Style hookMeaning
[data-value]The numeric value represented by the endpoint.
[data-active]The endpoint currently reached by pointer or keyboard.

ChartTooltip

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

Keep exploring