Menu


Donut chart


Two to eight parts of a whole as a ring, with the total in the middle and a legend beside it that names each part with its value.

Customize the preview


Show
View
  • Design$1,357
  • Hosting$1,283
  • Tools$650
  • Others$1,794
Spend by product, last 7 days
PartValueShare
Design$1,35726.7%
Hosting$1,28325.2%
Tools$65012.8%
Others$1,79435.3%

Options


The donut chart has no choices: it is drawn one way.

Examples


Usage

A week's spend by product: the total in the middle, and each product with its amount in the legend, in the order the ring runs clockwise from the top.

  • Design$1,357
  • Hosting$1,283
  • Tools$650
  • Others$1,794
Spend by product, last 7 days
PartValueShare
Design$1,35726.7%
Hosting$1,28325.2%
Tools$65012.8%
Others$1,79435.3%

Three parts

Three parts take the first three colors.

  • Design$1,357
  • Hosting$1,283
  • Tools$650
Spend by product, three parts
PartValueShare
Design$1,35741.2%
Hosting$1,28339%
Tools$65019.8%

Two parts

Two parts.

  • Design$1,357
  • Hosting$1,283
Spend by product, two parts
PartValueShare
Design$1,35751.4%
Hosting$1,28348.6%

Six parts

Each part takes its own color, in the order the ring runs.

  • Design$1,357
  • Hosting$1,283
  • Tools$650
  • Support$912
  • Social$540
  • Training$342
Spend by product, six parts
PartValueShare
Design$1,35726.7%
Hosting$1,28325.2%
Tools$65012.8%
Support$91217.9%
Social$54010.6%
Training$3426.7%

More than eight parts

The ninth part and those after it are gathered as Other, in gray. The table under the chart, shown here, still lists each part with its share.

  • Design$1,357
  • Hosting$1,283
  • Tools$650
  • Support$912
  • Social$540
  • Training$342
  • Legal$280
  • Events$240
  • Other$310
Data
Spend by product, ten parts
PartValueShare
Design$1,35722.9%
Hosting$1,28321.7%
Tools$65011%
Support$91215.4%
Social$5409.1%
Training$3425.8%
Legal$2804.7%
Events$2404.1%
Travel$1903.2%
Misc.$1202%

Without the legend

The ring alone, for a card that names the parts elsewhere. A screen reader still hears the summary and reads the table.

Spend by product, without the legend
PartValueShare
Design$1,35726.7%
Hosting$1,28325.2%
Tools$65012.8%
Others$1,79435.3%

Guidelines


Purpose

Shows how a whole splits into two to eight parts, such as a week's spend by product, with the total in the middle and each part's value beside the ring.

When to use

  • A total and the two to eight parts it is made of, where the reader wants the parts' sizes against the whole.

When not to use

  • For parts that must be compared exactly, or that are close in size. Use a bar chart, or the numbers.
  • For change over time. Use a line chart or a bar chart.
  • For more than eight parts. Gather the smallest, or use a bar chart.
  • When one number answers the question. Show the number.

Usage rules

Do

  • Keep to a few parts, eight at most, and give each its value in the legend.
  • Put the period or the unit under the total, such as "7 days".
  • Keep the figure to ten characters. Give a longer total a shorter form with figure, such as $1.3M.
  • Say the largest part and its share in the summary.
  • Give each part the color it has on the page's other charts, so a reader matches them at a glance.

Don't

  • Leave out the legend: color alone cannot name the parts.
  • Use a donut for parts that are nearly the same size.
  • Put a value only in the picture: the legend and the table hold them.
  • Design$1,357
  • Hosting$1,283
  • Tools$650
Spend by product, do
PartValueShare
Design$1,35741.2%
Hosting$1,28339%
Tools$65019.8%
Do

Keep to a few parts and name each with its value, so the reader need not judge the angles.

Spend by product, don't
PartValueShare
Design$1,35741.2%
Hosting$1,28339%
Tools$65019.8%
Don't

Without the legend, colors alone are left to tell the parts apart.

Anatomy

  • The block: the ring and the legend side by side, the table after them.
  • The ring part, the picture: the svg part with a part for each part of the whole, clockwise from the top, its color a modifier of the part; the center part holding the figure part, the total unless given, and the caption part under it. A long figure takes a size modifier of the figure part, --size-md or --size-sm; the wrapper sets it, and markup written by hand adds it.
  • The legend part: a list of legend items, each part's name and value, in the order the ring runs.
  • The table part: each part's value and share in a table, for screen readers; or the data part, the same table in a closed disclosure under the chart.

Behavior

The ring keeps one size; the legend takes the room beside it. The parts run clockwise from the top in the eight series colors, in a fixed order, a small gap between each. A ninth part and the rest are gathered as Other, in gray. A part can name its own color (swatch, a number from 1 to 8), so it keeps it whatever its place, the same color it has on the page's other charts; the parts that name none take the colors left, in order, and swatch="muted" gathers a part into Other. A part with nothing in it draws nothing, and stays in the legend unless legendEmpty is turned off; the table lists it either way. The middle shows the total unless the page gives another figure, with an optional caption such as the period. A figure too long for its full size steps down, so it keeps clear of the ring: $5,084 stays as it is, $99,999 takes a smaller heading and $1,250,000 a label. The middle holds ten characters. In a narrow layout the legend goes under the ring. The first time the chart comes into view the ring sweeps round from the top, while the middle and the legend stay, and later changes to the data land at once. Under reduced motion, and in markup written by hand, it appears at once. With a mouse, pointing at a part of the ring fades the other parts, so the one pointed at stands out. Pointing at a part also shows the browser's own tooltip, with its name, value and share; hand-written markup puts the same words in a title element inside the part's path. Nothing takes focus unless the chart is made focusable: then the parts are one stop for the Tab key, the arrow keys move round the ring, and the focused part is outlined in the focus color while the others fade, as when pointed at. A focusable part is named by its figures instead of showing the browser's tooltip, which would be read a second time. onActiveChange tells the page the part pointed at or focused, with its name and value, and null when there is none, so the page can show it in a readout. The wrapper writes the values and shares in English unless locale names another language or format writes the values, so a page prepared on a server shows the same numbers once its script runs.

Accessibility


What it does for you

  • The ring is an image named by the summary.
  • The legend is a list a screen reader reads, each part with its value.
  • Every part is in a table titled by the chart's label, with its value and its share of the whole. The table lists each part, even those gathered as Other in the picture. It is out of sight, for screen readers, unless the page shows it in the disclosure under the chart, which the keyboard reaches.
  • The parts differ by color, so the legend names them in the order the ring runs, with their values; the eight series colors are chosen to stay apart for readers with color blindness, the first and the last too, which meet at the top.
  • In high contrast mode the ring keeps its colors, so each part still matches the legend.
  • Nothing in the ring takes focus unless made focusable; the legend and the table name every part and value. Made focusable, the ring is a group named by the summary and each part an image named by its name, value and share, one stop for the Tab key.

What you must do

  • Write the summary as the chart's main message: the total and the largest part, with its share.
  • In markup written by hand, name the ring with role="img" and its summary, and keep the legend and the table. For parts that take focus, make the ring role="group" named by the summary and the drawing role="presentation", give each part role="img", its figures in aria-label and a tabindex, 0 on one and -1 on the rest, and move focus with the arrow keys yourself.
  • When the parts take focus, show the figures of the part pointed at or focused near the chart, from onActiveChange: sighted readers no longer get the browser's tooltip.

How to test

  • With a screen reader, the ring is read as an image by its summary, then the legend as a list with each part and its value.
  • Show the data: Tab to the disclosure under the chart and open it with the keyboard. The table gives each part with its value and share.
  • With the colors removed, the legend still names every part and its value.
  • With text at 200% and the page 320 pixels wide, the legend goes under the ring and stays whole.
  • In a forced-color mode, each part of the ring still matches the legend.
  • With parts that take focus, Tab once to reach the ring, move round it with the arrow keys, and hear each part's name, value and share.

Keyboard

Donut chart keys
KeyWhat it does
Tab
Moves focus to the parts when they take focus, to the part focused last or else the first, then to the disclosure under the chart when the page shows the data.
Right arroworDown arrow
On a part, moves focus to the next part, clockwise.
Left arroworUp arrow
On a part, moves focus to the part before.
HomeorEnd
On a part, moves focus to the first or the last part.
EnterorSpace
Opens or closes the disclosure that shows the table.

Source


The files the donut chart is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.

Stylesheet
donut-chart.css
/* smbk-donut-chart — Donut chart. Figma "Donut chart" 7580:1283.
 * A ring of two to eight parts, clockwise from the top in the categorical colors, a small gap between each, the
 * figure in the middle with its caption under it, and a legend beside the ring naming each part with its value.
 * A ninth part and the rest are gathered as one, in the muted gray. In a narrow layout the legend goes under
 * the ring. The first time the chart comes into view the ring sweeps round from the top; pointing at a part fades
 * the others. Nothing takes focus, unless the parts are made focusable: then a focused part is outlined in the
 * focus color and the others fade. */

.smbk-donut-chart {
  display: flex;
  align-items: center;
  gap: var(--smbk-space-inline-xl);
  min-width: 0;
  max-width: var(--smbk-size-width-400);
}

/* The block frames the words it hides for screen readers, so they stay inside it wherever it is placed: without a
   frame they hang from the page itself and stretch it, past the end of an area that scrolls. No weight, so a parent
   that places the block keeps its say. */
:where(.smbk-donut-chart) {
  position: relative;
}

.smbk-donut-chart__ring {
  position: relative;
  flex: none;
  width: var(--smbk-size-chart-donut);
  height: var(--smbk-size-chart-donut);
}

.smbk-donut-chart__svg {
  display: block;
  width: 100%;
  height: 100%;
}

.smbk-donut-chart__part {
  fill: var(--smbk-color-data-categorical-1);
  stroke: none;
}

/* ---- each part's color ---- */

.smbk-donut-chart__part--swatch-2 { fill: var(--smbk-color-data-categorical-2); }
.smbk-donut-chart__part--swatch-3 { fill: var(--smbk-color-data-categorical-3); }
.smbk-donut-chart__part--swatch-4 { fill: var(--smbk-color-data-categorical-4); }
.smbk-donut-chart__part--swatch-5 { fill: var(--smbk-color-data-categorical-5); }
.smbk-donut-chart__part--swatch-6 { fill: var(--smbk-color-data-categorical-6); }
.smbk-donut-chart__part--swatch-7 { fill: var(--smbk-color-data-categorical-7); }
.smbk-donut-chart__part--swatch-8 { fill: var(--smbk-color-data-categorical-8); }
.smbk-donut-chart__part--swatch-muted { fill: var(--smbk-color-surface-track); }

/* The figure and its caption, centred in the hole. */
.smbk-donut-chart__center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.smbk-donut-chart__figure {
  color: var(--smbk-color-text-default);
  font-family: var(--smbk-typography-heading-xs-font-family);
  font-size: var(--smbk-typography-heading-xs-font-size);
  font-weight: var(--smbk-typography-heading-xs-font-weight);
  line-height: var(--smbk-typography-heading-xs-line-height);
  letter-spacing: var(--smbk-typography-heading-xs-letter-spacing);
  text-transform: var(--smbk-typography-heading-xs-text-transform);
  font-variant-numeric: tabular-nums;
}

/* A long figure steps down, so it keeps clear of the ring: the wrapper sets the size from how far the figure runs, the
   smaller heading first and a label after it. The heading the ring draws reaches it at seven characters. */
.smbk-donut-chart__figure--size-md {
  font-family: var(--smbk-typography-heading-2xs-font-family);
  font-size: var(--smbk-typography-heading-2xs-font-size);
  font-weight: var(--smbk-typography-heading-2xs-font-weight);
  line-height: var(--smbk-typography-heading-2xs-line-height);
  letter-spacing: var(--smbk-typography-heading-2xs-letter-spacing);
  text-transform: var(--smbk-typography-heading-2xs-text-transform);
}

.smbk-donut-chart__figure--size-sm {
  font-family: var(--smbk-typography-label-lg-font-family);
  font-size: var(--smbk-typography-label-lg-font-size);
  font-weight: var(--smbk-typography-label-lg-font-weight);
  line-height: var(--smbk-typography-label-lg-line-height);
  letter-spacing: var(--smbk-typography-label-lg-letter-spacing);
  text-transform: var(--smbk-typography-label-lg-text-transform);
}

.smbk-donut-chart__caption {
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-xs-font-family);
  font-size: var(--smbk-typography-body-xs-font-size);
  font-weight: var(--smbk-typography-body-xs-font-weight);
  line-height: var(--smbk-typography-body-xs-line-height);
  letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
  text-transform: var(--smbk-typography-body-xs-text-transform);
}

.smbk-donut-chart__legend {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--smbk-space-stack-sm);
  min-width: 0;
  margin: 0;
  padding: 0;
}

/* The table, for screen readers only, unless the page shows it in the disclosure under the chart. */
/* .smbk-donut-chart__table: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */

/* A table takes the room its rows need whatever width and height it is given, so the hidden one is full size, only
   unseen. It hangs from the block's end corner, reaching back and up, the way a page never scrolls: it adds no room
   to the block or to any area that scrolls around it, and stays a table for screen readers. */
.smbk-donut-chart__table {
  inset-block-end: 0;
  inset-inline-end: 0;
}

/* The disclosure that shows the table takes the chart's whole width, under the ring and the legend. */
.smbk-donut-chart:has(> .smbk-donut-chart__data) {
  flex-wrap: wrap;
}

.smbk-donut-chart__data {
  flex: 1 0 100%;
}

/* A narrow space puts the legend under the ring. */
@container style(--smbk-layout: narrow) {
  .smbk-donut-chart {
    flex-direction: column;
    align-items: stretch;
    max-width: none;
  }

  .smbk-donut-chart__ring {
    align-self: center;
  }
}

/* ---- pointing at a part ----
   Where there is a mouse, pointing at a part of the ring fades every other part to the faded opacity, half, so the
   one pointed at stands out; with nothing pointed at, the parts wait the hover close delay before coming back, so
   crossing the thin gap between two parts never flashes the whole ring; the fade moves with the feedback purpose, under reduced motion too, since nothing moves. The middle
   lets the pointer through to the ring under it. Nothing is told by the fade alone: the legend and the table name
   every part with its value. Parts take no focus unless made focusable (below). At rest every part is
   whole. */

@media (hover: hover) {
  .smbk-donut-chart__center {
    pointer-events: none;
  }

  .smbk-donut-chart__svg:has(.smbk-donut-chart__part:hover) .smbk-donut-chart__part:not(:hover) {
    opacity: var(--smbk-opacity-faded);
    transition-delay: 0s;
  }

  .smbk-donut-chart__svg:has(.smbk-donut-chart__part:hover) .smbk-donut-chart__part:hover {
    transition-delay: 0s;
  }

  .smbk-donut-chart__part {
    transition: opacity var(--smbk-motion-feedback-duration) var(--smbk-motion-feedback-easing) var(--smbk-motion-hover-close-delay);
  }
}

/* ---- a part that takes focus ----
   Parts made focusable (a tabindex on each, one of them 0, and each named by its figures) draw the focus ring as an
   edge in the focus color along the part's own outline, the ring's width on screen whatever the chart's size, and a
   focused part stands out as a pointed one does: every other part fades to the faded opacity, on any screen, since
   a keyboard has no hover. */

.smbk-donut-chart__part:focus {
  outline: none;
}

.smbk-donut-chart__part:focus-visible {
  stroke: var(--smbk-color-border-focus);
  stroke-width: var(--smbk-focus-ring-width);
  vector-effect: non-scaling-stroke;
}

.smbk-donut-chart__svg:has(.smbk-donut-chart__part:focus-visible) .smbk-donut-chart__part:not(:focus-visible) {
  opacity: var(--smbk-opacity-faded);
}

/* The ring reaches the drawing's edge, so the edge's outer half would be cut off there. */
.smbk-donut-chart__svg:has(.smbk-donut-chart__part:focus-visible) {
  overflow: visible;
}

/* ---- motion: the entrance ----
   The wrapper marks the chart `data-entrance` the first time it comes into view (simbolik/components/entrance.ts):
   waiting while it is out of view, the ring hidden; playing once it is seen, the ring uncovered clockwise from the
   top, the way its parts run, with the chart entrance purpose: a conic mask whose edge turns from nothing to the
   whole circle. --smbk-donut-chart-sweep is that edge, as a share of the circle; it is registered so it can move
   smoothly. The middle and the legend stay. Without the mark (markup written by hand, the rendered pictures, after
   the entrance) the ring is simply drawn, and under reduced motion nothing moves. Only the mask changes, so
   nothing shifts. The mask reads only how opaque its paint is, so the text color stands in for "shown". */

@property --smbk-donut-chart-sweep {
  syntax: "<number>";
  inherits: false;
  initial-value: 100;
}

@media screen and (prefers-reduced-motion: no-preference) {
  .smbk-donut-chart[data-entrance] .smbk-donut-chart__svg {
    mask-image: conic-gradient(currentcolor calc(var(--smbk-donut-chart-sweep) * 1%), transparent 0);
  }

  .smbk-donut-chart[data-entrance="waiting"] .smbk-donut-chart__svg {
    --smbk-donut-chart-sweep: 0;
  }

  .smbk-donut-chart:is([data-entrance="load"], [data-entrance="playing"]) .smbk-donut-chart__svg {
    animation: smbk-donut-chart-sweep var(--smbk-motion-chart-entrance-reveal-duration) var(--smbk-motion-chart-entrance-easing) backwards;
  }
}

@keyframes smbk-donut-chart-sweep {
  from {
    --smbk-donut-chart-sweep: 0;
  }
}

/* ---- high contrast (forced colors) ---- */
/* The parts keep their colors, so each still matches its legend swatch. */

@media (forced-colors: active) {
  .smbk-donut-chart__svg {
    forced-color-adjust: none;
  }

  .smbk-donut-chart__part:focus-visible {
    stroke: Highlight;
  }
}
Tokens it reads
Donut chart tokens
Token and CSSValuePreviewFoundation
space.inline.xl--smbk-space-inline-xlcomfortable: 16pxcompact: 12pxchanges with densitySpacing
size.width.400--smbk-size-width-400400pxSize
size.chart.donut--smbk-size-chart-donut140pxSize
color.data.categorical.1--smbk-color-data-categorical-1light: default: #3b5ce3 · cockpit: #0a85a1dark: default: #7fa0ff · cockpit: #34a2bfchanges with themeColor
color.data.categorical.2--smbk-color-data-categorical-2light: default: #da601a · cockpit: #b5771cdark: default: #fa8349 · cockpit: #c28535changes with themeColor
color.data.categorical.3--smbk-color-data-categorical-3light: default: #4e555e · cockpit: #8e4170dark: default: #c4cdd8 · cockpit: #a35d85changes with themeColor
color.data.categorical.4--smbk-color-data-categorical-4light: default: #269936 · cockpit: #11957cdark: default: #54b85b · cockpit: #3ca88fchanges with themeColor
color.data.categorical.5--smbk-color-data-categorical-5light: default: #ae5bf9 · cockpit: #6f59a6dark: default: #dac3f7 · cockpit: #8270b5changes with themeColor
color.data.categorical.6--smbk-color-data-categorical-6light: default: #137575 · cockpit: #688e3bdark: default: #1d9393 · cockpit: #7ea059changes with themeColor
color.data.categorical.7--smbk-color-data-categorical-7light: default: #968507 · cockpit: #4167addark: default: #968507 · cockpit: #5a7dbcColor
color.data.categorical.8--smbk-color-data-categorical-8light: default: #c41883 · cockpit: #a04f27dark: default: #f8bbd7 · cockpit: #b06846changes with themeColor
color.surface.track--smbk-color-surface-tracklight: #7c8692dark: #4e555echanges with themeColor
color.text.default--smbk-color-text-defaultlight: #232527dark: #e7ebf0changes with themeColor
typography.heading-xs--smbk-typography-heading-xs-font-family--smbk-typography-heading-xs-font-size--smbk-typography-heading-xs-font-weight--smbk-typography-heading-xs-line-height--smbk-typography-heading-xs-letter-spacing--smbk-typography-heading-xs-text-transformfontFamily: Funnel Display, Funnel Display Fallback, system-ui, sans-seriffontSize: min: 20px · max: 28pxfontWeight: 400lineHeight: 1.2letterSpacing: 0px

The quick brown fox

Typography
typography.heading-2xs--smbk-typography-heading-2xs-font-family--smbk-typography-heading-2xs-font-size--smbk-typography-heading-2xs-font-weight--smbk-typography-heading-2xs-line-height--smbk-typography-heading-2xs-letter-spacing--smbk-typography-heading-2xs-text-transformfontFamily: Funnel Display, Funnel Display Fallback, system-ui, sans-seriffontSize: min: 18px · max: 24pxfontWeight: 400lineHeight: 1.2letterSpacing: 0px

The quick brown fox

Typography
typography.label-lg--smbk-typography-label-lg-font-family--smbk-typography-label-lg-font-size--smbk-typography-label-lg-font-weight--smbk-typography-label-lg-line-height--smbk-typography-label-lg-letter-spacing--smbk-typography-label-lg-text-transformfontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 16pxfontWeight: 500lineHeight: 1.4letterSpacing: 0px

The quick brown fox

Typography
color.text.subtle--smbk-color-text-subtlelight: #4e555edark: #9ba5b1changes with themeColor
typography.body-xs--smbk-typography-body-xs-font-family--smbk-typography-body-xs-font-size--smbk-typography-body-xs-font-weight--smbk-typography-body-xs-line-height--smbk-typography-body-xs-letter-spacing--smbk-typography-body-xs-text-transformfontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px

The quick brown fox

Typography
space.stack.sm--smbk-space-stack-smcomfortable: 8pxcompact: 6pxchanges with densitySpacing
opacity.faded--smbk-opacity-faded0.5Opacity
motion.feedback.duration--smbk-motion-feedback-duration150msMotion
motion.feedback.easing--smbk-motion-feedback-easingcubic-bezier(0.16, 1, 0.3, 1)Motion
motion.hover.close-delay--smbk-motion-hover-close-delay320msMotion
color.border.focus--smbk-color-border-focuslight: #cc0d0ddark: #f34336changes with themeColor
focus.ring.width--smbk-focus-ring-width2pxFocus
motion.chart-entrance.reveal-duration--smbk-motion-chart-entrance-reveal-duration1000msMotion
motion.chart-entrance.easing--smbk-motion-chart-entrance-easingcubic-bezier(0, 0.7, 0.5, 1)Motion
Manifest
donut-chart.manifest.json
{
  "name": "donut-chart",
  "block": "smbk-donut-chart",
  "summary": "Two to eight parts of a whole as a ring, with the total in the middle and a legend beside it that names each part with its value.",
  "adapt": "css",
  "axes": {},
  "flags": {},
  "parts": [
    "ring",
    "svg",
    "part",
    "center",
    "figure",
    "caption",
    "legend",
    "table",
    "data"
  ],
  "figmaNodeId": "7580:1283"
}