Data display
Metric
One number people watch. The unit is set smaller and quieter than the number itself: "42 s" at one size reads as one word, and once size and colour are parted the eye grabs the number and the "s" recedes.
Markup
<div class="inst-metric-row">
<div class="inst-metric">
<div class="inst-metric-label">Runs per day</div>
<div class="inst-metric-value">128</div>
<div class="inst-metric-delta" data-dir="down" data-tone="ok">18% against the last</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Average time</div>
<div class="inst-metric-value">4.2<span class="inst-metric-unit">s</span></div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Warnings</div>
<div class="inst-metric-value">5</div>
<div class="inst-metric-delta" data-dir="up" data-tone="error">5 new</div>
</div>
</div>
Markup
<div class="inst-metric-row inst-metric-row--joined">
<div class="inst-metric">
<div class="inst-metric-label">Runs per day</div>
<div class="inst-metric-value">128</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Average time</div>
<div class="inst-metric-value">4.2<span class="inst-metric-unit">s</span></div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Warnings</div>
<div class="inst-metric-value">5</div>
</div>
</div>
Markup
<div class="inst-metric-row inst-metric-row--joined inst-metric-row--headline">
<div class="inst-metric">
<div class="inst-metric-label">Success rate</div>
<div class="inst-metric-value">94.2<span class="inst-metric-unit">%</span></div>
<div class="inst-metric-delta" data-dir="down" data-tone="warn">1.8 points in a week</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Duration p95</div>
<div class="inst-metric-value">4:12<span class="inst-metric-unit">min</span></div>
<div class="inst-metric-delta">median 2:48</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">In the queue</div>
<div class="inst-metric-value">37</div>
<div class="inst-metric-delta" data-dir="up" data-tone="warn">9 waiting over an hour</div>
</div>
</div>
Contract
| What | Required | Why |
|---|---|---|
--joined for a band that is one fact |
yes | Separate tiles say "a set of numbers", one parted surface says "the state of this screen". Picking the wrong one is not a style slip but a wrong sentence |
inst-metric-label |
yes | "128" with no answer to "of what" is not a metric |
| The sign of the change in words in the delta | yes | The arrow is a second carrier rather than the only one: "↓ 18%" reads without a tone too |
| An accessible name from the label and the number | yes, if the metric is clickable | Otherwise "128" is spoken with no answer to "of what" |
data-dir and data-tone |
no | Without the first the delta goes with no arrow, without the second in a neutral colour |
Accessibility
| The label and the number | Tied visually by their order. If the metric is clickable, the accessible name has to include both — otherwise "128" is spoken with no answer to "of what" |
| Not colour alone | The delta carries an arrow and the sign of the change in the text. A reader who does not tell tones apart reads "↓ 18%" |
| Contrast | The delta takes --tone-ink — the threshold of text at 4.5:1 rather than of a mark at 3:1 |
| The size of the number | --text-lg inside a region, and the tile keeps a value/label ratio of 1.44. A metric there may not outrank the name of the screen it stands on — measured 2026-08-27. The one exception is --headline, below |
--headline only under the page header, never inside a region |
yes |
At most one --headline band per screen |
yes |
| Tabular figures | On by default: a number updating in place does not tug at its neighbours |
Anatomy
Direction and judgement are different attributes
This is the main trap of the component. An arrow down on the time of a run is good, an arrow up on warnings is bad. An attribute named after the arrow and meaning the judgement is a guaranteed mistake of application.
| Attribute | What it encodes | Values |
|---|---|---|
data-dir |
Which way it changed | up · down |
data-tone |
Whether that is good or bad | ok · warn · error · neutral · running |
Markup
<div class="inst-metric-row">
<div class="inst-metric">
<div class="inst-metric-label">Run time</div>
<div class="inst-metric-value">42<span class="inst-metric-unit"> s</span></div>
<div class="inst-metric-delta" data-dir="down" data-tone="ok">18% — it fell, and that is good</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Warnings</div>
<div class="inst-metric-value">12</div>
<div class="inst-metric-delta" data-dir="up" data-tone="error">5 new — it grew, and that is bad</div>
</div>
</div>
Both attributes are optional: a delta with no data-dir goes with no arrow,
with no data-tone in a neutral colour.
Composition
A row of metrics — with no borders
Markup
<div class="inst-metric-row">
<div class="inst-metric">
<div class="inst-metric-label">Total</div>
<div class="inst-metric-value">128</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Succeeded</div>
<div class="inst-metric-value">121</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Failed</div>
<div class="inst-metric-value">7</div>
</div>
</div>
Four numbers are one group. A border around each turns them into four objects, between which the reader starts looking for differences that are not there. So a metric stands on a muted surface rather than in a card.
Patterns
The summary of a run
On its own a metric is just a large number; the meaning appears in a row.
What is visible only here:
- none of them has a border. Four numbers are one group; a border around each would turn them into four objects, between which the reader starts looking for differences;
- the unit is smaller and quieter than the number. "42 s" at one size reads as one word; once size and colour are parted the eye grabs the number and the "s" recedes;
- direction and judgement are independent. An arrow down on the time is good, an arrow up on warnings is bad.
Markup
<div class="inst-metric-row">
<div class="inst-metric">
<div class="inst-metric-label">Run time</div>
<div class="inst-metric-value">42<span class="inst-metric-unit"> s</span></div>
<div class="inst-metric-delta" data-dir="down" data-tone="ok">18% against the last</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Tests passed</div>
<div class="inst-metric-value">248<span class="inst-metric-unit">/251</span></div>
<div class="inst-metric-delta" data-tone="warn">3 failing</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Bundle size</div>
<div class="inst-metric-value">7.4<span class="inst-metric-unit"> MB</span></div>
<div class="inst-metric-delta">unchanged</div>
</div>
<div class="inst-metric">
<div class="inst-metric-label">Warnings</div>
<div class="inst-metric-value">12</div>
<div class="inst-metric-delta" data-dir="up" data-tone="error">5 new</div>
</div>
</div>
API
| Name | What it does |
|---|---|
| class | |
inst-metric | One metric |
inst-metric-row | A row of metrics |
inst-metric-label | The label above the number |
inst-metric-value | The number itself |
inst-metric-unit | The unit inside the number: smaller and quieter |
inst-metric-delta | The change against the previous period |
| modifier | |
inst-metric-row--joined | One surface parted by rules instead of separate tiles |
inst-metric-row--headline | The band under a page header. The value takes the ceiling of the scale. One per screen |
| Name | Value | What it does |
|---|---|---|
| attribute | ||
data-dir | up · down | on inst-metric-delta |
data-tone | neutral · running · ok · warn · error | in the same place |
| Name | Value | Where from |
|---|---|---|
| variable | ||
--metric-value-sizeHow loud the value is | --text-lg | the application sets it |
| token | ||
--text-2xl | 1.75rem | base +4: scale |
--text-2xs | 0.6875rem | base +4: scale |
--surface-recessed | light-dark(oklch(0 0 0 / 0.060), oklch(1 0 0 / 0.050)) | one across every cell |
--radius-md | 8px | base +4: scale |
--pad-block-y | var(--space-4) | base +14: density · scale |
--gap-row | var(--space-3) | base +14: density · scale |
--text-lg | 1.125rem | base +4: scale |
--text-xs | 0.78125rem | base +4: scale |
--tracking-tight | -0.01em | one across every cell |
--tone-ink | var(--accent-text) | base +2: |
--size-chevron | 10px | base +4: scale |
--space-2 | 4px | one across every cell |
Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument