Spending against a budget, with spent and held in one bar and a marker for today's pace.
Segments
Each segment is a share of max, stacked from the start of the bar. A solid fill shows what is spent, and ink stripes show what is held, committed but not gone. The rest of the track is the frame color. Anything past max is clipped, and a segment with no room left is not drawn.
Example
Example
import { Card, Meter } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function MeterBasic(): ReactElement {
return (
<div className="sample-column">
<Card>
<Meter
label="Groceries"
max={1200}
segments={[
{ value: 820, look: 'fill', label: 'Spent', valueLabel: '$820.00' },
{ value: 200, look: 'hatched', label: 'Held', valueLabel: '$200.00' },
]}
valueLabel="$1,020 of $1,200"
/>
</Card>
</div>
);
}Pace and legend
pace places a marker where spending would be today if it kept pace. hasLegend adds a row under the bar naming each segment with its value, then the marker.
Example
Example
- Spent$210.00
- Held$40.00
- Where you'd be today: $260
import { Card, Meter } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function MeterPace(): ReactElement {
return (
<div className="sample-column">
<Card>
<Meter
label="Transport"
max={400}
segments={[
{ value: 210, look: 'fill', label: 'Spent', valueLabel: '$210.00' },
{ value: 40, look: 'hatched', label: 'Held', valueLabel: '$40.00' },
]}
valueLabel="$250 of $400"
pace={{ value: 260, label: "Where you'd be today: $260" }}
hasLegend
/>
</Card>
</div>
);
}Over the limit
When the segments add up to more than max, both looks turn the warning color and the bar fills to its end. valueLabel must say "over" in words, such as "$340 of $300, $40 over", since color is never the only signal.
Example
Example
- Spent$280.00
- Held$60.00
import { Card, Meter } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
export default function MeterOver(): ReactElement {
return (
<div className="sample-column">
<Card>
{/* Over the limit the bar turns the warning color, and the words say so: color is never the only signal. */}
<Meter
label="Dining out"
max={300}
segments={[
{ value: 280, look: 'fill', label: 'Spent', valueLabel: '$280.00' },
{ value: 60, look: 'hatched', label: 'Held', valueLabel: '$60.00' },
]}
valueLabel="$340 of $300, $40 over"
hasLegend
/>
</Card>
</div>
);
}On another surface
The cut between segments and the ring around the pace marker use --meter-surface, which defaults to Lifted, the card a meter usually sits on. On the page itself, set --meter-surface: var(--color-canvas) on the meter or any ancestor, and every meter inside follows. A DataTable sets it for you, to each row's own fill, hovered and selected included.
.budget-section {
--meter-surface: var(--color-canvas);
}Meter or Gauge
Both are meters: a measurement of the reader's state on a known range. Meter is a bar for a row or a list, with segments that stack, such as spent and held, and a pace marker. Gauge is one value drawn as a ring or a semicircle, for a glance: a credit score with its word under it, a utilization in a table row. Meter's range starts at 0 and ends at max, since its segments stack; Gauge's runs from minValue to maxValue, so a score can run from 300 to 850. A meter's cuts and pace ring take --meter-surface; a gauge's marks stand in gaps its svg cuts out, so it needs no surface color. Work getting done is neither: that is a ProgressBar.
Usage
Pass every figure as a formatted string. The library formats no numbers, so the server's HTML is final in any locale. The meter is static, so a server component renders it with no client JavaScript.
import { Meter } from '@snapshot-ui/react';
<Meter
label="Groceries"
max={1200}
segments={[
{ value: 820, look: 'fill', label: 'Spent', valueLabel: '$820.00' },
{ value: 200, look: 'hatched', label: 'Held', valueLabel: '$200.00' },
]}
valueLabel="$1,020 of $1,200"
pace={{ value: 900, label: "Where you'd be today: $900" }}
hasLegend
/>;Props
Meter accepts every HTML element attribute, on its root. The name (aria-label or aria-labelledby) goes on the bar.
aria-label unknown | |
|---|---|
aria-labelledby unknown | |
className string | |
hasLegend boolean | Draws a legend under the bar for the segments and the pace. Default: false |
label ReactNode | The visible label, such as "Groceries". When absent, |
max number | The limit, in the same units. Over 0: a limit of 0 or below draws an empty track and reports 0. |
pace { value: number; label: string; } | Where spending would be today if it kept pace, in the same units. |
segments readonly MeterSegment[] | |
valueLabel string | The visible value text and the accessible value: "$1,020 of $1,200". Over the limit it must say so in words. |
label string | For the legend and the accessible value: "Spent", "Held". |
|---|---|
look "fill" | "hatched" |
|
value number | In the meter's units, such as dollars. Segments stack in order from the start. |
valueLabel string | Formatted: "$820.00". |
Accessibility
The bar is a role="meter" from 0 to max, named by the label or aria-label.
- Its value is read as
valueLabel, never as a percentage. The visible value text is hidden, since the meter reads it. - The pace label is read as its description.
- The legend is a plain list outside the meter, since a meter's children are presentational.
- It takes no focus and has no keys.
Design rules
What keeps meters in the system.
Say "over" in the value text when spending passes the limit.
Hatch what is held, so it reads as committed but not gone.
Set --meter-surface on a page that is not a card.
Never let color alone say a meter is over.
Never stack more than three segments.
Never use a meter for a task that is finishing. That is a progress bar.