Navigation

v0.0.4
Data display

Breakdown

A whole as its parts along one bar, each as wide as its share.

On this page

A breakdown shows a whole as its parts along one bar, each as wide as its share: storage by kind, a bill by service, a budget and what is left of it. It is a list. Every item carries its number once for the drawing and its name and figure as text, and the stylesheet draws the bar and the legend under it from that one list. There is no script and no SVG.

Breakdown

<div class="card max-w(440) mx-auto">  <div class="card-header">    <h3 class="card-title">Storage</h3>    <p class="card-description">90 GB of 128 GB used</p>  </div>  <ul class="breakdown" aria-label="Storage by kind">    <li class="breakdown-item" style="--value: 42">      <span class="breakdown-label">Photos        <span class="breakdown-value">42 GB</span>      </span>    </li>    <li class="breakdown-item" style="--value: 28">      <span class="breakdown-label">Apps        <span class="breakdown-value">28 GB</span>      </span>    </li>    <li class="breakdown-item" style="--value: 14">      <span class="breakdown-label">Mail        <span class="breakdown-value">14 GB</span>      </span>    </li>    <li class="breakdown-item" style="--value: 6">      <span class="breakdown-label">System        <span class="breakdown-value">6 GB</span>      </span>    </li>    <li class="breakdown-item breakdown-rest" style="--value: 38">      <span class="breakdown-label">Free        <span class="breakdown-value">38 GB</span>      </span>    </li>  </ul></div>
Point at a part: the others step back.

#How a part gets its width

Each breakdown-item sets --value in its style, and its segment grows by that number, so every part takes its share of their sum. The values need not be percentages and need not add up to anything: gigabytes, dollars and counts all work as they are. A share of nothing keeps a 2px tick, so a part that exists is never invisible.

An item has no box of its own. Its segment joins the bar and its breakdown-label joins the legend, which wraps under the bar; the list keeps its items, so it still reads as data to assistive technology.

Segments are steps of one ink, darkest first. Order the parts from largest to smallest and keep to five or so: the eye tells five tones apart, not eight.

#What is left

breakdown-rest marks the part that is not used: free space, the rest of a budget, seats not yet taken. Its segment is a well instead of ink and its dot in the legend is a ring, so used and unused are told apart by material, not by a shade of grey. Put it last.

#Rows

breakdown-rows sets the legend as rows on hairlines, one to a part, with each figure at the end. Use it when the figures are read and compared, as on an invoice. --breakdown-height makes the bar thicker.

Rows

<div class="card max-w(340) mx-auto">  <div class="card-header">    <h3 class="card-title">November</h3>    <p class="card-description">$3,074 so far</p>  </div>  <ul class="breakdown breakdown-rows" style="--breakdown-height: .625rem" aria-label="Spend by service">    <li class="breakdown-item" style="--value: 1840">      <span class="breakdown-label">Compute        <span class="breakdown-value">$1,840</span>      </span>    </li>    <li class="breakdown-item" style="--value: 920">      <span class="breakdown-label">Storage        <span class="breakdown-value">$920</span>      </span>    </li>    <li class="breakdown-item" style="--value: 310">      <span class="breakdown-label">Network        <span class="breakdown-value">$310</span>      </span>    </li>    <li class="breakdown-item" style="--value: 4">      <span class="breakdown-label">Support        <span class="breakdown-value">$4</span>      </span>    </li>  </ul></div>

#A part with a status

The bar is monochrome. When one part is a status, such as requests that failed, set --breakdown-fill on that item to a status color, and name the status in its label: the hue never carries the meaning alone. --breakdown-color changes the ink every segment is a step of.

Status

<div class="card max-w(440) mx-auto">  <div class="card-header">    <h3 class="card-title">Requests</h3>    <p class="card-description">1.28M in the last hour</p>  </div>  <ul class="breakdown" aria-label="Requests by result">    <li class="breakdown-item" style="--value: 962">      <span class="breakdown-label">Succeeded        <span class="breakdown-value">96.2%</span>      </span>    </li>    <li class="breakdown-item" style="--value: 31">      <span class="breakdown-label">Redirected        <span class="breakdown-value">3.1%</span>      </span>    </li>    <li class="breakdown-item" style="--value: 7; --breakdown-fill: var(--color-danger)">      <span class="breakdown-label">Failed        <span class="breakdown-value">0.7%</span>      </span>    </li>  </ul></div>

#Breakdown or chart

A breakdown answers "what is this made of": the parts are read against their sum. A chart answers "which is larger": the bars are read against each other and a scale. One amount within a range is a meter.

#Accessibility

  • Write it as a <ul>, or an <ol> when the order is a ranking, and name it with aria-label or a heading.
  • Put the name and the figure of every part in its breakdown-label, with the unit. The bar is drawn by the stylesheet and is not announced.
  • Do not rely on the tones to tell parts apart: the legend names each one, in the same order as the bar. In forced colors every segment is the text color and the 2px gaps part them.

#Classes

ClassDescription
breakdownThe list: an 8px bar of segments 2px apart, and a legend that wraps under it
breakdown-rowsThe legend as rows on hairlines, one to a part, with each figure at the end. With breakdown
breakdown-itemOne part; set --value in its style. It has no box of its own: its segment joins the bar and its label joins the legend. Segments are steps of one ink, darkest first
breakdown-restThe part that is not used, such as free space: a well instead of ink, and a ring instead of a dot in the legend. With breakdown-item
breakdown-labelThe part's name in the legend, after a dot of its segment's color
breakdown-valueThe part's figure as text, muted, inside breakdown-label

#Custom properties

PropertyDefaultDescription
--valueThe part's number; the stylesheet reads 0 when it is not set. On breakdown-item
--breakdown-height.5remThickness of the bar
--breakdown-gap2pxSpace between segments
--breakdown-colorvar(--color-surface-inverse)The ink the segments are steps of
--breakdown-fillOne segment's color, and its dot's. By default a step of --breakdown-color by the item's place; set it for a part that carries a status. On breakdown-item

#Source

@master/ui/components/breakdown.css
/* * Breakdown: a whole as its parts along one bar, each as wide as its share. * The stylesheet draws it from a list. Each item carries its number as --value and its name in * breakdown-label; the item itself has no box, so its segment joins the bar and its name joins the legend * under it, and the list still reads as data to anyone who is not looking at it. The parts need not add up * to anything: each takes its share of their sum. * Segments are steps of one ink, darkest first, 2px apart, and the bar's two ends are round. The part that * is not used (free space, what is left of a budget) is breakdown-rest: a well instead of ink. * Under the pointer the other parts step back, at once, as a chart's bars do. */@layer components {  .breakdown {    --breakdown-height: .5rem;    --breakdown-gap: 2px;    --breakdown-color: var(--color-surface-inverse);    display: flex;    flex-wrap: wrap;    align-items: center;    min-width: 0;    margin: 0;    padding: 0;    color: var(--color-text-body);    font: 400 .8125rem/1.25rem var(--font-family-sans);    font-feature-settings: inherit;    list-style: none;    /* The end of the bar's line: everything after it is the legend. */    &::before {      content: '';      order: 1;      flex: none;      width: 100%;      height: 0;    }    /* Rows: one line to a part, its figure at the end, for a legend that is read like a table. */    &.breakdown-rows {      .breakdown-label {        flex: none;        width: 100%;        margin: 0;        padding-block: .4375rem;      }      .breakdown-item:not(:last-child) .breakdown-label {        border-bottom: 1px solid var(--color-line-hair);      }      .breakdown-item:first-child .breakdown-label {        margin-top: .5rem;      }      .breakdown-value {        margin-inline-start: auto;      }    }    @media (hover: hover) {      /* The tint is the bar's ink stepped back, so a part that has its own color steps back with the others. */      &:has(.breakdown-item:hover) .breakdown-item:not(:hover, .breakdown-rest) {        --breakdown-fill: color-mix(in oklab, var(--breakdown-color) 9%, transparent);      }      .breakdown-item:hover .breakdown-label, .breakdown-item:hover .breakdown-value {        color: var(--color-text-strong);      }    }    @media (forced-colors: active) {      --breakdown-color: CanvasText;    }  }  /* One part. It has no box of its own: its segment and its name are laid out by the list. */  .breakdown-item {    --breakdown-tone: 100%;    --breakdown-fill: color-mix(in oklab, var(--breakdown-color) var(--breakdown-tone), transparent);    display: contents;    &:nth-child(2) { --breakdown-tone: 60%; }    &:nth-child(3) { --breakdown-tone: 36%; }    &:nth-child(4) { --breakdown-tone: 21%; }    &:nth-child(n + 5) { --breakdown-tone: 12%; }    /* The segment: its share of the bar. A share of nothing keeps a 2px tick. */    &::before {      content: '';      flex-grow: var(--value, 0);      flex-shrink: 0;      flex-basis: 0;      min-width: 2px;      height: var(--breakdown-height);      margin-inline-end: var(--breakdown-gap);      border-radius: 1.5px;      background: var(--breakdown-fill);      transition: flex-grow var(--duration-base) var(--easing-glide);      @media (prefers-reduced-motion: reduce) {        transition: none;      }      @media (forced-colors: active) {        forced-color-adjust: none;      }    }    &:first-child::before {      border-start-start-radius: 999px;      border-end-start-radius: 999px;    }    &:last-child::before {      margin-inline-end: 0;      border-start-end-radius: 999px;      border-end-end-radius: 999px;    }    /* What is left: a well, not ink, and a ring instead of a dot beside its name. */    &.breakdown-rest {      &::before {        background: var(--color-surface-inset);        box-shadow: var(--shadow-pressed);      }      .breakdown-label::before {        background: var(--color-surface-inset);        box-shadow: inset 0 0 0 .5px var(--color-line-mark);      }      @media (forced-colors: active) {        &::before {          outline: 1px solid CanvasText;          outline-offset: -1px;          background: Canvas;        }      }    }  }  /* The part's name in the legend, after a dot of its segment's color. */  .breakdown-label {    display: inline-flex;    order: 2;    align-items: center;    gap: .4375rem;    min-width: 0;    margin-block: .625rem 0;    margin-inline: 0 1.125rem;    white-space: nowrap;    /* It answers the pointer, and it is not a line to select: the arrow stays an arrow over it. */    cursor: default;    &::before {      content: '';      flex: none;      width: .5rem;      height: .5rem;      border-radius: calc(.125rem * var(--corner-scale));      corner-shape: var(--corner-shape);      background: var(--breakdown-fill);      @media (forced-colors: active) {        forced-color-adjust: none;      }    }  }  /* The part's figure, inside its name. */  .breakdown-value {    color: var(--color-text-muted);    font-variant-numeric: tabular-nums;  }}