Design System

Design System

Meter

Indicators

Open in Storybook: Meter (opens in a new tab)

On this page

Overview

A native meter for a read-only measurement inside a known range: fuel, storage used, a battery, a score. The browser maps min, max and value to the accessibility tree, so the component adds only what the browser leaves out: the status in words and a shape, the value in words, and the design around it. The browser paints the zone and says nothing about it, so the status is worked out from low, high and optimum.

It is never focusable and never interactive. A value the user sets is a range, a task moving forward is a progress, and a rating the user gives is a radio group.

Anatomy

  1. Label

    label for

    Names the meter. Always visible.

  2. Track

    meter, ::-webkit-meter-bar

    The range from min to max. The meter holds the role, the name, the value and the range.

  3. Fill

    ::-webkit-meter-optimum-value and its two siblings

    Painted by the browser from low, high and optimum: purple is the good zone, yellow the middle, red the worst.

  4. Status

    p aria-hidden="true" with an svg

    Where the value is, in a word and a shape (check, dash, warning), so colour is never the only cue.

  5. Value text

    span aria-hidden="true"

    The visible copy of the value. Hidden because the meter already exposes it.

  6. Marks

    div aria-hidden="true"

    A custom design: squares or stars, one for each unit. The native meter sits invisible over them.

Usage

Use cases

The eight of the design, each the real component, and each works: move the slider under it to try a value and see the zone, the word and what a screen reader says.

  • Fuel level

    Higher is better.

    80 out of 100, good

    Says “80 out of 100, good”.

    min=0 max=100 low=33 high=66 optimum=80 value=80

  • Storage used

    Lower is better.

    12.4 of 16 GB used, filling up

    Says “12.4 of 16 GB used, filling up”.

    min=0 max=16 low=8 high=13 optimum=4 value=12.4

  • Battery

    Higher is better, warn when low.

    15%, low

    Says “15%, low”.

    min=0 max=100 low=20 high=80 optimum=100 value=15

  • Password strength

    0 to 4, higher is better.

    Weak

    Says “Weak”.

    min=0 max=4 low=2 high=3 optimum=4 value=1

  • Coffee intensity

    Custom squares, no zones.

    7 out of 10

    Says “7 out of 10”.

    min=0 max=10 value=7

  • Product rating

    Static info only.

    4 out of 5 stars

    Says “4 out of 5 stars”.

    min=0 max=5 value=4

  • Accessibility score

    Higher is better.

    92 out of 100, good

    Says “92 out of 100, good”.

    min=0 max=100 low=50 high=90 optimum=100 value=92

  • Room humidity

    Middle is best.

    45%, comfortable

    Says “45%, comfortable”.

    min=0 max=100 low=30 high=60 optimum=45 value=45

Props

Props
PropDoesDefault
labelNames the meter. Always visibleRequired
valueThe measurementRequired
minLowest value of the range0
maxHighest value of the range. Required: left alone the browser makes it 1 and a value of 7 reads fullRequired
low · high · optimumCut the range in three regions and say which one is goodThe browser's: min, max, the midpoint
lookbar, squares or stars. The marks are one for each unit, under an invisible native meterbar
wordsWhat each region is called: { low, middle, high }. With them the field shows the status as a word and a shape, and the word goes into aria-valuetextNone: no status
valueTextThe value in words, (value, max) => string. Empty when the status word is the whole text"80 out of 100"
shownThe visible copy of the value, (value, max) => string, or null for none"80 / 100"

Example

words is what turns the zone into a word and a shape. The meter says "20 out of 100, low".

import { Meter } from '@/components/Meter'

<Meter
  label="Fuel level"
  value={20}
  max={100}
  low={33}
  high={66}
  optimum={80}
  words={{ low: 'Low', middle: 'Medium', high: 'Good' }}
/>

Specs

Measurements

Measurements
PartPropertyValue
FieldGap8px · 12px (0.5rem · 0.75rem)
LabelText13px (0.8125rem) / 1.45, 700
BarHeight · corner20px (1.25rem) · 6px
BarBorder2px solid
FillEdge2px line on its end
SegmentSize · gap10 × 20px · 3px
StarSize · gap20 × 20px, 1.5px line · 4px
StatusText · icon13px (0.8125rem) / 1.4, 600 · 16px icon, 6px gap
Value textText12px (0.75rem) / 1.45, monospace, tabular numbers
Label · value textWidth from 48rem9rem · 11rem at the least

Zones

low and high cut the range in three. optimum says which region is the good one, and the other two are ranked by their distance from it. The word and the icon follow the same rule as the fill, so they never disagree with it.

Zones
ZoneWhere the value isFillIconWord
BestIn the optimum regionPurpleCheck in a circlePurple
MiddleOne region from itYellowDash in a circleInk
WorstOn the far sideRedWarning triangleRed

Zone by optimum

Zone by optimum
Optimum isValue below lowValue betweenValue above high
Between low and highmiddlebestmiddle
Below low (lower is better)bestmiddleworst
Above high (higher is better)worstmiddlebest

Layout

The breakpoints are container queries in rem, so they follow the room the field has and the size of the text: a field in a narrow card stacks on a wide screen. One DOM, only the CSS changes. Each use case is a story in Storybook. Pick the width in the viewport menu of the toolbar, for any story: 640 CSS px · 200% zoom or 320 CSS px · 400% zoom.

Layout
BreakpointCSS widthReached byLayout
≥ 48rem≥ 768pxDesktop at 100–150% zoomOne row: label · bar · value text
30–48rem480–767px1280px at 200% (640 CSS px) · tablets · text at 200%Label and value text on one row, bar full width below
< 30rem< 480px1280px at 400% (320 CSS px) · phonesStacked: label, bar, value text. No sideways scroll

Attributes

Attributes
AttributeMeaningDefault
minLowest value of the range0
maxHighest value of the range. Always set1
valueThe measurement, kept between min and max0
lowUpper edge of the low regionmin
highLower edge of the high regionmax
optimumThe best valueThe midpoint
aria-valuetextThe value in words, with the statusNone: the number is read

Bar parts

Bar parts
PartWebKit · BlinkFirefox
Wrapper::-webkit-meter-inner-element—
Track::-webkit-meter-barThe element itself
Fill, good zone::-webkit-meter-optimum-value:-moz-meter-optimum::-moz-meter-bar
Fill, middle zone::-webkit-meter-suboptimum-value:-moz-meter-sub-optimum::-moz-meter-bar
Fill, worst zone::-webkit-meter-even-less-good-value:-moz-meter-sub-sub-optimum::-moz-meter-bar

Markup

<label for="fuel">Fuel level</label>
<meter id="fuel" min="0" max="100" low="33" high="66" optimum="80" value="20"
       aria-valuetext="20 out of 100, low">20 out of 100, low</meter>
<p aria-hidden="true"><svg focusable="false">…</svg> Low · 20 / 100</p>

Script

const WORDS = { low: 'Low', middle: 'Medium', high: 'Good' }   // by region, never by colour

// A value on a boundary joins the optimum's region, as in the browsers
function where({ low, high, optimum, value }) {
  const side = (n) => (n < low ? 'low' : n > high ? 'high' : 'middle')
  const good = side(optimum)
  const region =
    good === 'low' ? (value <= low ? 'low' : value <= high ? 'middle' : 'high')
    : good === 'high' ? (value >= high ? 'high' : value >= low ? 'middle' : 'low')
    : side(value)
  const zone = region === good ? 'best' : region === 'middle' || good === 'middle' ? 'middle' : 'worst'
  return { region, zone }
}

function render(meter, status) {     // meter.low, .high, .optimum: the browser resolves them
  const { region, zone } = where(meter)
  const text = `${meter.value} out of ${meter.max}, ${WORDS[region].toLowerCase()}`
  meter.setAttribute('aria-valuetext', text)
  meter.textContent = text
  status.dataset.zone = zone          // picks the icon and the colour
  status.lastChild.textContent = ` ${WORDS[region]} · ${meter.value} / ${meter.max}`
}

Tokens

Component tokens

Component tokens
PartPropertyTokenValue
BarBorder widthbw2px
BarBorder and fill edge colourink#16161a
BarCorner—6px
TrackFill—#e6e6ea
Fill, bestFilllilac#5e548e
Fill, middleFillmarker#ffe600
Fill, worstFilldanger#82002a
Segment, onFill—#5d5fef
Segment, offBorder · fill— · white#6b6b75 · #ffffff
StarFilled · outline—#5d5fef · #6b6b75
LabelText colourink#16161a
Value textText colourink-soft#4b4b55
Status, bestText and iconlilac#5e548e
Status, middleText · iconink · marker#16161a · #ffe600
Status, worstText and icondanger#82002a

Guidelines

Content

Content
PartFormatExample
LabelWhat is measured"Fuel level" · "Storage"
Value textThe value, then what it is of, in words"80 out of 100" · "12.4 of 16 GB used" · "7 out of 10"
Status wordOne for each side of the range, by what it means and not by colour"Low · Medium · Good" · "Too dry · Comfortable · Too humid"
Visible valueShort, in the same unit"80 / 100" · "12.4 / 16 GB" · "15%"

Behavior

Behavior
EventResult
Any change of the valuevalue, aria-valuetext, the text inside the meter and the status change together
A value on a boundaryIn the optimum region when it touches it, as the browser paints it
A value outside the rangeThe browser clamps it. The marks never show more than the range
No low, high or optimumThe browser defaults. A custom design has no zones and no status
MarksOne for each unit of the range, the first ones lit. 3.5 lights four; the value text keeps 3.5
Status word with no numberThe word is the whole value text: "Weak"

Alternatives

Alternatives
ElementUse it forThe user changes itRole
meterA measurement inside a known rangeNometer
progressCompletion of a taskNoprogressbar
input type="range"Picking a value on a continuous scaleYesslider
Radio groupPicking one of a few options, a rating the user givesYesradiogroup, radio
Plain textA number with no known range ("1,204 followers")No—

Accessibility

Keyboard

Keyboard
KeyActionNative
TabSkips the meter: it is not a tab stopYes
Arrow keysNothing: the value is not the user's to changeYes

Roles and properties

Roles and properties
PropertyExposedNote
RolemeterNative. Never role="meter" on a div
Name"Fuel level"From label for. aria-labelledby when the name is in several elements, aria-label only with no visible text
Value"80 out of 100, good"aria-valuetext. Without it the number alone is read. Never aria-valuenow
Rangemin 0 · max 100From the native attributes
Fallback"80 out of 100, good"The text inside meter, for browsers without it
HiddenStatus, value text, marksaria-hidden="true", and focusable="false" on an svg
Tab stops0No tabindex, onclick or role="button"

Screen reader

A meter is static: it shows a value, not the progress of that value, and announces nothing. A changed value is read when the user reaches the meter. The exact words differ by screen reader, browser and platform.

Screen reader
MomentWhat is read
Reach the meterIts role, its name, aria-valuetext and the range: "Fuel level", meter, "80 out of 100, good"
The value changesNothing. The new value is read the next time the meter is reached
The page loadsNothing

Measured behavior

Chromium 148 and 149, WebKit 26.4.

Measured behavior
ObservationConsequence
With the optimum below low, a value equal to low is painted as the good zone. Above high, a value equal to high isWords and icon use the same edges. A rule that tests value < low and value > high is wrong on the edges
The accessibility tree has the meter with its name, value, min, max and aria-valuetext, and not the status, the value text, the squares or the starsHide every copy with aria-hidden
A meter at opacity: 0 stays in the tree, name and value text includedStretch it over a custom design. Never display: none or visibility: hidden
The yellow fill is 1.02:1 against the trackThe 2px ink edge on the end of the fill shows where it stops, and the value is in words as well
The bar and its three zones look the same in WebKit as in ChromiumOne stylesheet with both sets of pseudo-elements

Support

The Firefox rules follow the design and have not been run.

Support
BrowserBar partsChecked
Chromium::-webkit-meter-*Yes
WebKit (Safari)::-webkit-meter-*Yes
Firefox::-moz-meter-bar with :-moz-meter-optimum, -sub-optimum, -sub-sub-optimumYes

WCAG criteria

WCAG criteria
CriterionRequirement here
1.1.1 Non-text Content (A)The marks, the stars and the icons are decoration, aria-hidden. The meter carries the information
1.3.1 Info and Relationships (A)label for names the meter. Role, value and range are native
1.4.1 Use of Color (A)The zone is a word and a shape as well as a colour, and is in aria-valuetext
1.4.3 Contrast (Minimum) (AA)Label 18:1, value text 8.6:1, status words 6.7:1 (purple) and 10.6:1 (red)
1.4.4 Resize Text (AA)Sizes in rem, no fixed height on the field
1.4.10 Reflow (AA)Stacked under 30rem. No sideways scroll at 320 CSS px
1.4.11 Non-text Contrast (AA)Bar border and fill edge 18:1, empty marks 5.3:1, filled marks 4.8:1
1.4.12 Text Spacing (AA)Label, status and value text wrap instead of clipping
4.1.2 Name, Role, Value (A)Native meter, label for, aria-valuetext

Related

This section is still in progress.

References

This section is still in progress.