Legends & data keys
@ponchia/ui/css/legend.css is an opt-in layer of data keys for charts,
reports, and analytical figures. A legend maps a visual encoding (colour, a
pattern, a shape, a gradient) to its meaning. It reads the same Tier-4
--chart-* tokens the data-viz palette ships, so a key never
drifts from the series it describes.
@import '@ponchia/ui';
@import '@ponchia/ui/css/dataviz.css';
@import '@ponchia/ui/css/legend.css';
Bronto paints and positions the key; it owns no scales, no data→pixel mapping, and no series state. Pair it with any chart — a token-themed inline SVG, or an external engine like Vega-Lite — the host owns the chart.
What it is not
- Not a chart engine. It does not compute tick positions, "nice" numbers, bin thresholds, or bubble radii — you supply the tick/range text; the CSS positions the slots you give it.
- Not a colour source. Swatch colour always comes from a
--chart-*token (enforced bycheck:legend), never a hand-rolled hex.
Accessibility: colour is never the only channel
A legend that distinguishes series by colour alone fails
WCAG 1.4.1 Use of Color.
In a legend, the text label is the required non-colour channel — every
entry carries its name, so the meaning survives even if colour is lost
(forced-colors, monochrome print, colour-vision deficiency). Where the chart
mark itself is colour-only (a bare area or line), also pair the swatch with
its --chart-pattern-* so the figure — not just the legend — stays readable.
Recommended structure: wrap the figure and its key in a <figure> with a
<figcaption>, and give the legend its own group label.
<figure role="group" aria-labelledby="fig-1-title">
<figcaption id="fig-1-title">Fig 1 — Weekly focus split</figcaption>
<!-- … the chart … -->
<ul class="ui-legend" aria-label="Series">
<li class="ui-legend__item">
<span
class="ui-legend__swatch"
style="--chart-color: var(--chart-3); --chart-pattern: var(--chart-pattern-3)"
aria-hidden="true"
></span>
<span class="ui-legend__label">Research</span>
</li>
<li class="ui-legend__item">
<span class="ui-legend__swatch ui-legend__swatch--2" aria-hidden="true"></span>
<span class="ui-legend__label">Delivery</span>
</li>
</ul>
</figure>
The swatch is decorative (aria-hidden="true") — the meaning is the label
text. Set its colour either inline (--chart-color) to mirror exactly what the
chart mark uses, or with a --N index helper for the categorical palette.
Parts
| Class | Role |
|---|---|
ui-legend |
The container (a wrapping inline row by default). |
ui-legend__title |
Optional heading for the key. |
ui-legend__item |
One entry — swatch + label (+ value). |
ui-legend__swatch |
The colour/pattern chip. |
ui-legend__symbol |
A glyph/shape chip (fill an .ui-icon mask). |
ui-legend__label |
The series name (the non-colour channel). |
ui-legend__value |
Optional trailing value/range. |
ui-legend__caption |
Optional footnote (units, source). |
ui-legend__track |
The gradient bar (continuous keys). |
ui-legend__ticks / ui-legend__tick |
Tick labels under the track. |
Swatch colour
| Approach | Use |
|---|---|
style="--chart-color: var(--chart-3)" |
Mirror any token, any order; add --chart-pattern to match a patterned mark. |
class="… ui-legend__swatch--3" |
Categorical palette series 1–8 — sets --chart-3 for you. |
ui-legend__swatch--circle and ui-legend__swatch--line change the chip shape
(dot series, line series).
A ui-legend__symbol chip is an .ui-icon mask — it needs a --icon-mask or it
paints a solid square, like any icon. There is no --glyph-*
token: build the mask value with
renderGlyph(name, { render: 'mask' }) from @ponchia/ui/glyphs,
e.g. el.style.setProperty('--icon-mask', renderGlyph('circle', { render: 'mask' }).match(/--icon-mask:([^"]+)/)[1]),
or render the whole chip with renderGlyph('circle', { render: 'mask', label }) and
add the ui-legend__symbol class to it. And an interactive legend entry must be a real
<button> (as in the example below) — a non-button ui-legend__item carrying
data-series is not keyboard-reachable.
Keying the de-emphasised series. In an accent-rationed chart — one mark
painted with brontoVegaAccent, the rest left
quiet with brontoVegaNeutral — the quiet neutral is the last categorical
series, --chart-8 (#4d5358). So a legend that honestly mirrors that chart
uses ui-legend__swatch--1 for the highlighted entry and
ui-legend__swatch--8 for the "everything else" entry — both are real palette
tokens, so the key never drifts from the marks and check:legend stays happy.
Don't hand-roll a grey: --chart-8 is the neutral by construction.
Variants
| Modifier | Effect |
|---|---|
ui-legend--vertical |
Stack entries instead of the wrapping row. |
ui-legend--compact |
Denser type and gaps. |
ui-legend--with-values |
Align a trailing __value column across rows. |
ui-legend--gradient |
Continuous colour ramp (__track + __ticks). |
ui-legend--diverging |
Use the 7-stop diverging ramp (with --gradient). |
ui-legend--threshold |
Binned swatch │ range-label grid. |
ui-legend--interactive |
Entries are toggle controls (see below). |
The recipe mirrors this surface:
import { ui } from '@ponchia/ui/classes';
ui.legend({ type: 'gradient', diverging: true });
// "ui-legend ui-legend--gradient ui-legend--diverging"
ui.legendSwatch({ series: 3, shape: 'circle' });
// "ui-legend__swatch ui-legend__swatch--3 ui-legend__swatch--circle"
Continuous ramp
Supply the min/mid/max tick text; the track interpolates the sequential
ramp in OKLCH (--diverging swaps in the diverging ramp around its neutral
centre).
<div class="ui-legend ui-legend--gradient" role="group" aria-label="Density">
<span class="ui-legend__track" aria-hidden="true"></span>
<span class="ui-legend__ticks">
<span class="ui-legend__tick">0</span>
<span class="ui-legend__tick">50</span>
<span class="ui-legend__tick">100</span>
</span>
</div>
Interactive legends (optional)
An interactive legend toggles a series on or off. Bronto ships the control surface only; the host owns the data. The split is deliberate (it keeps the legend out of chart-engine territory):
- Bronto: each entry is a
<button aria-pressed>. The optionalinitLegendbehavior flipsaria-pressed, toggles.is-inactive, and dispatchesbronto:legend:togglewith{ detail: { series, active } }. - You: listen for the event, hide/show your own series, and announce the
change through an
aria-liveregion you own.
<ul class="ui-legend ui-legend--interactive" data-bronto-legend aria-label="Series">
<li>
<button type="button" class="ui-legend__item" aria-pressed="true" data-series="research">
<span class="ui-legend__swatch ui-legend__swatch--1" aria-hidden="true"></span>
<span class="ui-legend__label">Research</span>
</button>
</li>
</ul>
import { initLegend } from '@ponchia/ui/behaviors';
const stop = initLegend(); // returns a cleanup fn
document.addEventListener('bronto:legend:toggle', (e) => {
const { series, active } = e.detail;
// hide/show your series, then announce it in your own aria-live region
});
Convention: aria-pressed="true" means the series is shown (the default).
The entry's label never changes on toggle — only aria-pressed and
.is-inactive flip, so a screen reader reads a stable name with a clear
pressed state. React/Solid/Qwik/Svelte consumers can use useLegend() (or the
legend Svelte action) instead of calling initLegend directly; Vue consumers
can use vLegend.