A bar that shows a measured share, such as capacity booked or budget used, coloured by your verdict on it (also called a gauge bar or level indicator). Reach for it in lists and tables where a percentage alone is hard to compare.
import { Meter } from "@umriss-ui/core";
value runs from 0 to 1 and is clamped outside; say the true figure beside the bar where it can pass 100 %.
The tone is your assessment, since only you know whether 90 % is good or bad; the bar never colours itself by height. Keep the number or a word beside it so the colour is never alone.
The role meter needs a name from you: label says what is measured, and neither the percentage nor a column heading counts as one.
A value from 0 to 1, a label naming what is measured, and showLabel for the percentage beside the bar.
import { Meter } from "@umriss-ui/core";
export default function AShare() {
return (
<div style={{ maxWidth: 320 }}>
<Meter value={0.8} label="Capacity booked, Arjun Mehta" showLabel />
</div>
);
}
The tone is your verdict on the value, not its height; the figure beside it says the same for whoever cannot see colour.
import { Grid, Meter, Stack, Text } from "@umriss-ui/core";
const CENTRES = [
{ name: "Sales", used: 0.62, tone: "neutral", note: "62 % of March's budget" },
{ name: "IT", used: 0.91, tone: "warning", note: "91 % – with a third of March left" },
{ name: "Marketing", used: 1.07, tone: "danger", note: "107 % – over budget" },
] as const;
export default function Tones() {
return (
<Grid minItemWidth="220px" gap={4}>
{CENTRES.map((centre) => (
<Stack key={centre.name} gap={2}>
<Text size="sm" weight="medium">
{centre.name}
</Text>
<Meter value={centre.used} tone={centre.tone} label={`Budget used, ${centre.name}`} />
<Text size="xs" tone="muted">
{centre.note}
</Text>
</Stack>
))}
</Grid>
);
}
In a table, one rule turns each share into a tone, and the hours beside the bar carry the value in words.
import { Meter, Text } from "@umriss-ui/core";
import type { MeterTone } from "@umriss-ui/core";
/** The rule: fully booked is fine, over 100 % warns, over 120 % is a problem. */
function toneOf(share: number): MeterTone {
if (share > 1.2) return "danger";
if (share > 1) return "warning";
return "neutral";
}
/** This week, 16 to 20 March: hours booked against hours available after leave. */
const ROWS = [
{ name: "Arjun Mehta", booked: 26, available: 16 },
{ name: "Chloe Durand", booked: 38, available: 40 },
{ name: "Noah Fischer", booked: 22, available: 24 },
{ name: "Hana Sato", booked: 44, available: 40 },
{ name: "Kofi Mensah", booked: 20, available: 32 },
{ name: "Freya Olsen", booked: 12, available: 24 },
].map((row) => ({ ...row, share: row.booked / row.available }));
const CELL = { padding: "var(--u-space-2) var(--u-space-3)", borderBottom: "1px solid var(--u-edge-color)" };
export default function TeamCapacity() {
return (
<table style={{ borderCollapse: "collapse", width: "100%", maxWidth: 560 }}>
<thead>
<tr>
<th style={{ ...CELL, textAlign: "left" }}>
<Text as="span" size="xs" tone="muted">Person</Text>
</th>
<th style={{ ...CELL, textAlign: "left", width: "45%" }}>
<Text as="span" size="xs" tone="muted">Booked this week</Text>
</th>
<th style={{ ...CELL, textAlign: "right" }}>
<Text as="span" size="xs" tone="muted">Hours</Text>
</th>
</tr>
</thead>
<tbody>
{ROWS.map(({ name, booked, available, share }) => (
<tr key={name}>
<td style={CELL}>
<Text as="span" size="sm">{name}</Text>
</td>
<td style={CELL}>
<Meter value={share} tone={toneOf(share)} showLabel label={`Capacity booked, ${name}`} />
</td>
<td style={{ ...CELL, textAlign: "right", whiteSpace: "nowrap" }}>
<Text as="span" size="sm" mono>
{booked} / {available}
</Text>
</td>
</tr>
))}
</tbody>
</table>
);
}
The role meter, from 0 to 100, with the whole percentage as its value. It is named by label, or by your aria-labelledby; without either it is named "Fill level", which says nothing about what is measured. The tone is not read out: say the verdict in a word beside the bar.
It is no live region: a new value is read when a reader reaches the bar. Under forced colours the track takes an edge and the fill the system's selection colour, whatever its tone.
MeterProps| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | number | — | Fill level from 0 to 1. Shown in: Configurator, A share, Tones and 3 more |
tone | MeterTone| "neutral" | "accent" | The colour of the bar. It comes from an assessment and not from taste:
beside the bar there is always the number or a word, so that the colour
is never the only statement. Shown in: Configurator, Tones, Compare a team's capacity and 2 more |
showLabel | boolean | true | The percentage to the right of the bar (Geist Mono). Shown in: Configurator, A share, Compare a team's capacity and 2 more |
label | string | the wording's "Fill level" | What is being measured, e.g. "Utilisation".
A role of meter needs a name from the author - the visible percentage
inside it does not count for that, and neither does the column heading
beside it. Whoever knows what the bar measures says it better than the
general term.Shown in: A share, Tones, Compare a team's capacity and 2 more |
ref | Ref<HTMLSpanElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
Also takes every attribute of <span>.
MeterToneThe colour of a meter's bar - an assessment, never the only statement.
type MeterTone = "neutral" | "accent" | "success" | "warning" | "danger";What umriss deliberately does not build, and why: ADR-0032.