Meter
Summary
A labeled value with a small track showing consumption against a
bound: budget spend, quota, capacity. It reads as a
level, not an activity: the substrate for the Cortex
cost-meter affordance (see the
Nware Cortex kit). Threshold coloring is the
consumer's call. Add -warning / -danger
when crossing 75% / 90%, or whatever the domain says.
When to use
| Form | Use it for |
|---|---|
.soma-meter | A fraction of a budget, quota or capacity, in a widget or drawer: storage used, monthly spend, seat count. |
.soma-meter-inline | A persistent affordance in the navbar or a toolbar, such as the always-visible cost meter. |
-warning / -danger | Crossing domain thresholds. The consumer applies the class; the meter just paints it. |
.soma-progress instead | Task completion, something running toward done. Meter = fraction of a budget; progress = fraction of a task. |
Examples
HTML
The full form puts the header (label + value) over the track. The
ARIA contract: role="meter" with
aria-valuenow / aria-valuemin /
aria-valuemax and an accessible name:
<div class="soma-meter" role="meter" aria-valuenow="82"
aria-valuemin="0" aria-valuemax="100" aria-label="Monthly budget">
<div class="soma-meter-header">
<span class="soma-meter-label">Budget</span>
<span class="soma-meter-value">$2,450 <small>of $3,000</small></span>
</div>
<div class="soma-meter-track"><div class="soma-meter-bar" style="width: 82%"></div></div>
</div>
Thresholds are extra classes on the root — the markup is otherwise identical:
<div class="soma-meter soma-meter-warning" role="meter" …> <!-- crossed 75% -->
<div class="soma-meter soma-meter-danger" role="meter" …> <!-- crossed 90% -->
For label-only, drop the value span but keep the numbers in the
aria-label so the meter still reads fully to AT:
<div class="soma-meter" role="meter" aria-valuenow="35"
aria-valuemin="0" aria-valuemax="100" aria-label="Disk usage 35% of 80 GB">
<div class="soma-meter-header">
<span class="soma-meter-label">Disk usage</span>
</div>
<div class="soma-meter-track"><div class="soma-meter-bar" style="width: 35%"></div></div>
</div>
The inline (cost-meter) form — same children, one extra class; header and track flatten onto a single row with a fixed 64px track:
<div class="soma-meter soma-meter-inline soma-meter-warning" role="meter"
aria-valuenow="82" aria-valuemin="0" aria-valuemax="100"
aria-label="Monthly budget 82% used">
<div class="soma-meter-header">
<span class="soma-meter-label">$42.10/d</span>
<span class="soma-meter-value">$2,450 <small>of $3,000</small></span>
</div>
<div class="soma-meter-track"><div class="soma-meter-bar" style="width: 82%"></div></div>
</div>
When the value changes, update the bar width,
aria-valuenow and the visible value together. See
the JavaScript section for the one function that does all
three plus the threshold class.
CSS classes
| Class | Effect |
|---|---|
.soma-meter | The root (min-width 120px). Carries the role="meter" ARIA contract above. |
.soma-meter-header | Baseline-aligned row: label at the start, value at the end. |
.soma-meter-label | Small uppercase label. |
.soma-meter-value | The current value; a nested <small> mutes the bound ("of $3,000"). Optional. Keep the numbers in aria-label when omitted. Rendered in the mono stack with tabular figures so a ticking readout doesn't shift the header. |
.soma-meter-track / .soma-meter-bar | 4px track and its fill. Set the bar's width inline. Width changes animate over 250ms (no transition under reduced motion). |
.soma-meter-warning / .soma-meter-danger | Threshold coloring on the root; -danger also tints the value text (via --soma-color-danger-subtle-fg, an AA text color in every theme). |
.soma-meter-inline | Compact horizontal form for the navbar: label · value · fixed 64px track on one row. Combines with the threshold classes. |
JavaScript
None as a component API — the meter is pure CSS. The consumer
owns the update: bar width, aria-valuenow, the
visible value and the threshold class all describe the same
number, so change them in one function or they drift. The
thresholds themselves are domain logic — 75%/90% here, but a
seat count might flip straight to danger at 100%:
function updateMeter(el, spent, cap, format) {
const fraction = Math.max(0, Math.min(1, spent / cap));
const percent = Math.round(fraction * 100);
el.querySelector('.soma-meter-bar').style.width = `${percent}%`;
el.setAttribute('aria-valuenow', String(percent));
el.setAttribute('aria-label', `Monthly budget ${percent}% used`);
el.querySelector('.soma-meter-value').innerHTML =
`${format(spent)} <small>of ${format(cap)}</small>`;
// Domain thresholds — the meter just paints whatever class it's given.
el.classList.toggle('soma-meter-warning', fraction >= 0.75 && fraction < 0.9);
el.classList.toggle('soma-meter-danger', fraction >= 0.9);
}
updateMeter(document.querySelector('#budget'), 2450, 3000,
(n) => '$' + n.toLocaleString());
Polling a live source: same function, on an interval; the bar animates between values on its own:
setInterval(async () => {
const { spent, cap } = await fetch('/api/budget').then((r) => r.json());
updateMeter(document.querySelector('#budget'), spent, cap, formatMoney);
}, 60_000);
The cost-meter affordance
The Cortex spec's CostMeter is this component — no wrapper, no
extra API. The composition: a -inline meter as a
persistent navbar affordance (burn rate as the label, spend
against cap as the value), next to the kill switch and the
notifications bell, exactly as the inline example above shows.
Crossing the domain's warning threshold flips -warning
on; a page-level banner (not the
meter) carries any "budget nearly exhausted" interruption. See
the Nware Cortex kit for the full
trust-primitive mapping.
The live-thresholds example above, exactly as this page wires
it (the same updateMeter shape, thresholds at
75%/90%):
const meter = document.getElementById('meter-live');
const CAP = 3000;
let spent = 1200;
function render() {
const fraction = Math.min(1, spent / CAP);
const percent = Math.round(fraction * 100);
meter.querySelector('.soma-meter-bar').style.width = `${percent}%`;
meter.setAttribute('aria-valuenow', String(percent));
meter.setAttribute('aria-label',
`Monthly budget $${spent.toLocaleString()} of $${CAP.toLocaleString()}`);
meter.querySelector('.soma-meter-value').innerHTML =
`$${spent.toLocaleString()} <small>of $${CAP.toLocaleString()}</small>`;
meter.classList.toggle('soma-meter-warning', fraction >= 0.75 && fraction < 0.9);
meter.classList.toggle('soma-meter-danger', fraction >= 0.9);
}
document.getElementById('meter-spend').addEventListener('click', () => {
spent = Math.min(CAP, spent + 450);
render();
});
document.getElementById('meter-reset').addEventListener('click', () => {
spent = 1200;
render();
});