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

Groceries
Follows the page theme and corners
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

Transport
  • Spent$210.00
  • Held$40.00
  • Where you'd be today: $260
Follows the page theme and corners
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

Dining out
  • Spent$280.00
  • Held$60.00
Follows the page theme and corners
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, aria-label or aria-labelledby names the meter.

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"

fill is solid ink, hatched is ink stripes: spent and held.

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.

esc

↑ ↓ to move↵ to open