← Back to gallery
CSSFeatured

Semantic Pie/Donut Chart Markup Strategy

Conic-gradient donut chart paired with a real <ol>/<li> legend so screen readers + copy-paste users see the data without color or motion. Three layouts: side-legend stat card, stacked overlay, and a centered KPI tile.

donut-chartconic-gradientol-legendfigcaptiondata-vizdecorative-svgaria-hidden

Data Viz / Semantic Donut

Semantic Pie/Donut Chart Markup Strategy

A decorative conic-gradient donut paired with readable text values so the chart meaning survives without the visual ring. Three visualization shapes, one accessible markup strategy.

Storage AllocationDocuments 64% · Photos 23% · Free 13%

Multi-segment breakdown

Storage Allocation

A 3-segment donut shows how a quota is split. The legend mirrors each slice in plain text so the breakdown survives without color or geometry.

  • conic-gradient
  • figcaption
  • multi-segment
Profile CompletionCompleteKeep going

Single-percentage progress

Profile Completion

A single-value progress ring uses one accent segment against a neutral track. The center label and figcaption both expose the same percentage so screen readers can read it cleanly.

  • progress
  • single-value
  • a11y
System StatusAll systems normal98% healthy

Compact dashboard indicator

System Status

A small inline ring sits beside a status sentence. The donut is purely decorative — the status text already communicates the value to assistive tech and reduced-motion fallbacks.

  • inline
  • status
  • compact

Donut inspector

Storage Allocation

Storage AllocationDocuments 64% · Photos 23% · Free 13%
  • conic-gradient
  • figcaption
  • multi-segment

A 3-segment donut shows how a quota is split. The legend mirrors each slice in plain text so the breakdown survives without color or geometry.

Helped you ship something? 🐟 Send my cat a churu

<figure>
  <div class="donut" aria-hidden="true"></div>
  <figcaption>
    <strong>Storage Allocation</strong>
    <span>Documents 64% · Photos 23%</span>
  </figcaption>
</figure>

.donut {
  --primary: 64;
  --secondary: 23;
  background: conic-gradient(
    #67e8f9 calc(var(--primary) * 1%),
    #a78bfa 0 calc((var(--primary) + var(--secondary)) * 1%),
    rgba(148, 163, 184, 0.18) 0
  );
  animation: donutSweep 2.40s ease-out both;
}

@media (prefers-reduced-motion: reduce) {
  .donut { animation: none; }
}

How to make this

A semantic CSS donut chart pairs an aria-hidden conic-gradient ring with a real figure caption and text values, so the data survives without color, geometry, or motion.

html
1<figure class="donut-chart" aria-labelledby="donut-title">2  <div class="donut-chart__ring" aria-hidden="true">    <span>64%</span>  </div>  <figcaption>    <strong id="donut-title">Storage allocation</strong>7    <ul>      <li>Documents 64%</li>      <li>Photos 23%</li>      <li>Free 13%</li>    </ul>  </figcaption></figure> <style>16@property --semantic-donut-progress {  syntax: "<number>";  inherits: false;  initial-value: 1;}.donut-chart {  --documents: 64;  --photos: 23;  display: grid;  justify-items: center;  gap: 1rem;  text-align: center;  color: #e5eefb;}.donut-chart__ring {  position: relative;  display: grid;  width: 10rem;  aspect-ratio: 1;  place-items: center;36  border-radius: 50%;  background: conic-gradient(    #38bdf8 calc(var(--semantic-donut-progress) * var(--documents) * 1%),    #a78bfa 0 calc(var(--semantic-donut-progress) * (var(--documents) + var(--photos)) * 1%),    #334155 0  );  animation: semantic-donut-sweep 1.8s ease-out both;}.donut-chart__ring::before {  content: "";  position: absolute;  inset: 1.4rem;  border-radius: inherit;  background: #0f172a;}.donut-chart__ring span {  position: relative;  z-index: 1;  font: 800 1.2rem/1 sans-serif;}.donut-chart ul {  display: grid;  gap: .35rem;  margin: .5rem 0 0;  padding: 0;  list-style: none;  color: #cbd5e1;}@keyframes semantic-donut-sweep {  from { --semantic-donut-progress: 0; }  to   { --semantic-donut-progress: 1; }}@media (prefers-reduced-motion: reduce) {69  .donut-chart__ring {    animation: none;    --semantic-donut-progress: 1;  }}</style>

Annotated snippet

  1. Line 1figure groups the visual chart with its textual explanation. aria-labelledby points the group at the real title instead of forcing the decorative ring to carry the chart meaning.
    PitfallShould a donut chart be exposed as an image to screen readers?

    Usually no. Treat the ring as decorative and expose the values as text in figcaption, a list, or a table. An image label like "64 percent donut chart" is less useful than the actual segment names and percentages.

  2. Line 2The ring is aria-hidden because it repeats the values already present in the caption. Screen readers should encounter the data once, in text, not as a decorative shape plus a second label.
    PitfallShould a donut chart be exposed as an image to screen readers?

    Usually no. Treat the ring as decorative and expose the values as text in figcaption, a list, or a table. An image label like "64 percent donut chart" is less useful than the actual segment names and percentages.

  3. Line 7The list is the source of truth for the slices. This keeps the chart understandable when color perception, high contrast mode, CSS support, or motion preferences make the ring less useful.

    A visual-only ring looks compact, but the labeled version communicates the data without relying on color or shape.

    PitfallIs color enough for a semantic pie or donut chart?

    No. Color is a visual aid, not the data model. Pair every slice with a text label and value, and avoid instructions that require users to distinguish similar hues. The text should still answer the chart question if the ring disappears.

  4. Line 16@property lets the numeric progress value animate cleanly from 0 to 1. Browsers without registered custom property animation should still see a correct final chart because the text values remain present.

    Simulated: without @property the browser cannot interpolate the numeric stop driving the fill, so the donut advances in discrete jumps. Registering @property as <number> lets the same value walk smoothly between keyframes and the ring sweeps cleanly.

    PitfallDo conic-gradient donut charts work in every browser?

    conic-gradient is supported in modern Chromium, Firefox, and Safari, but older browsers may fail to draw the ring. Registered custom property animation also has support caveats. Keep the textual values visible and let the chart degrade to a static or absent decoration.

  5. Line 36conic-gradient draws the slices from the same percentages used in the caption. The second color starts at 0 after the first stop, which avoids a visible seam between adjacent segments.
    PitfallDo conic-gradient donut charts work in every browser?

    conic-gradient is supported in modern Chromium, Firefox, and Safari, but older browsers may fail to draw the ring. Registered custom property animation also has support caveats. Keep the textual values visible and let the chart degrade to a static or absent decoration.

  6. Line 69Reduced motion skips the sweep and pins the chart to its final state. The fallback changes only the animation, not the data or the visible labels.
    PitfallHow should a donut chart handle prefers-reduced-motion?

    Skip the fill sweep and render the final percentages immediately. Do not hide the legend or caption, because those are the accessible representation of the data. Reduced motion should remove movement, not remove information.

Other pitfalls

Are animated donut sweeps expensive?
Animating a conic gradient can repaint, especially when driven by a changing custom property. Use it for small charts, avoid dozens of simultaneous sweeps, and stop at the final state. For dashboards with many charts, prefer static rings or SVG paths updated once.

Notes

Overview

Pie and donut charts are tempting to ship as pure SVG, but a chart is data first and decoration second. This pattern uses a conic-gradient donut for the ring (zero SVG, zero JS for the math) paired with a real <ol><li> legend so screen readers and copy-paste users see the actual numbers, not just the colored slices. The chart and the legend stay synchronized because both read from the same data source.

When to use it

Reach for the conic-gradient + legend approach when you have three to six slices of comparable size and the data is categorical (browser share, plan tier breakdown, traffic sources). Skip it for time-series data — line and bar charts read the change better. Skip it for more than six slices — the eye loses track of which color maps to which legend row past that, and a horizontal stacked bar becomes easier to scan.

How it works

Build the donut by setting background: conic-gradient(slice1 0deg X, slice2 X Y, ...) on a circular element, then carve out the centre with a smaller pseudo-element using the surface background color so a thin ring remains. Add border-radius: 50% to the wrapper so the slices clip to a circle instead of a square. The legend is a real <ol> with each <li> wrapping the swatch and the label; nothing about the legend needs the chart to render correctly, and vice versa. The DOM contract is: data drives both surfaces independently.

Production gotchas

conic-gradient stops accept degrees, percentages, or a mix — mix them and Chromium and Firefox interpolate the stops differently. Pick one unit and stay with it across every slice. If you animate the gradient via custom property interpolation, register the property with @property and syntax: "<angle>" or the browser will discrete-step the change. Donut hole punched with a pseudo-element is solid color — if the parent background is a gradient or image, switch the pseudo to backdrop-filter: blur(0) + appropriate masking, or the hole will look fake.

Accessibility

The donut is aria-hidden="true" — it is a decorative repeat of the legend. The <ol> is the data; screen-reader users hear the labels and percentages in order. If you animate the slice sizes (e.g. on data refresh), the visual transition is fine because the data itself is not animated — only its rendering. prefers-reduced-motion: reduce jumps directly to the new chart state rather than interpolating.

References

Implementation depth

The chart is decoration; the ordered legend is the data. conic-gradient can draw the donut quickly, but the labels, values, and reading order need to live in semantic markup that survives if the visual ring fails.

Keep slice counts low and units consistent. Mixing degrees and percentages makes stop maintenance harder, while too many slices turns the legend into the real chart anyway. In those cases, use a bar or table instead.