# Meter

> The native <meter> with low, high and optimum regions.

Page: https://svelte-lean.vercel.app/primitives/meter · Markdown: https://svelte-lean.vercel.app/primitives/meter.md · Index: https://svelte-lean.vercel.app/llms.txt

The native meter element, styled: a measurement within a known range, such as storage used or a password score, colored by how good it is.

```svelte
<script lang="ts">
	// No runtime: the browser owns the meter. The stylesheet is optional.
	import '@svelte-lean/styles/meter.css';

	const folders = [
		{ name: 'Photos', used: 42 },
		{ name: 'Mail', used: 78 },
		{ name: 'Backups', used: 96 }
	];
</script>

<div class="storage">
	{#each folders as folder (folder.name)}
		<label for="storage-{folder.name}">{folder.name}: {folder.used} GB of 100 GB</label>
		<meter
			id="storage-{folder.name}"
			data-slean="meter"
			min="0"
			max="100"
			low="70"
			high="90"
			optimum="0"
			value={folder.used}
		></meter>
	{/each}
</div>

<style>
	.storage {
		display: grid;
		gap: 0.5rem;
	}
</style>
```

One range with low 70, high 90 and optimum 0: the browser puts each value in its region and the stylesheet colors it.

## Examples

### Password strength

```svelte
<script lang="ts">
	import '@svelte-lean/styles/field.css';
	import '@svelte-lean/styles/input.css';
	import '@svelte-lean/styles/meter.css';

	const words = ['Very weak', 'Weak', 'Fair', 'Good', 'Strong'];
	const rules = [/.{12,}/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/];
	let password = $state('');
	const score = $derived(rules.filter((rule) => rule.test(password)).length);
</script>

<!-- The optimum is high: a low score is even less good. -->
<div data-slean="field" style="max-inline-size: 20rem">
	<label for="new-password" data-slean-part="label">Password</label>
	<input
		id="new-password"
		type="password"
		autocomplete="new-password"
		bind:value={password}
		data-slean="input"
	/>
	<meter
		data-slean="meter"
		min="0"
		max="4"
		low="2"
		high="3"
		optimum="4"
		value={score}
		aria-label="Password strength"
		aria-valuetext={words[score]}
	></meter>
	<p data-slean-part="description">{words[score]}</p>
</div>
```

Type a password: the score moves the meter through its three regions.

### Live value

```svelte
<script lang="ts">
	import '@svelte-lean/styles/meter.css';
	import '@svelte-lean/styles/slider.css';

	let charge = $state(64);
</script>

<div class="battery">
	<label for="battery">Battery: {charge}%</label>
	<meter
		id="battery"
		data-slean="meter"
		data-size="large"
		min="0"
		max="100"
		low="20"
		high="50"
		optimum="100"
		value={charge}
	></meter>
	<label for="charge">Charge</label>
	<input id="charge" type="range" data-slean="slider" min="0" max="100" bind:value={charge} />
</div>

<style>
	.battery {
		display: grid;
		gap: 0.5rem;
	}
</style>
```

Drag the slider below 50 and below 20: the browser moves the value into the next region.

### Sizes

```svelte
<script lang="ts">
	import '@svelte-lean/styles/meter.css';
</script>

<!-- No low and no high: one region, drawn in the current color (data-color, primary by default). -->
<div class="sizes">
	<meter data-slean="meter" data-size="small" value="0.6" aria-label="Small"></meter>
	<meter data-slean="meter" value="0.6" aria-label="Default"></meter>
	<meter data-slean="meter" data-size="large" value="0.6" aria-label="Large"></meter>
</div>

<style>
	.sizes {
		display: grid;
		gap: 1rem;
	}
</style>
```

## Usage

Plain markup with a stylesheet: install `@svelte-lean/styles`. No JavaScript runs.

```bash
npm install @svelte-lean/styles
```

`stylesheets`

```ts
import '@svelte-lean/styles/meter.css';
```

Set `min`, `max` and `value`; add `low`, `high` and `optimum` when some values are better than others. A low `optimum` makes high values bad (storage), a high one makes low values bad (a score).

Say the value in text as well, in the label or next to the meter: the color alone is not read, and it is lost under forced colors.

## Anatomy

| Part | Element | data-slean-part | Required | Notes |
| --- | --- | --- | --- | --- |
| root | `<meter data-slean="meter" min max low high optimum value>` | – | yes | Without low and high the value has one region and takes the current color. data-size="small\|large". A label through \<label for\>, aria-labelledby or aria-label. |

## Keyboard

| Key | When | Result |
| --- | --- | --- |
| `Tab` | anywhere | Skips the meter: it is not focusable (native) |

## Styling

`meter.css` makes the element the track, gives the inner pseudo-elements the element’s block size (a percentage height does not fill it in Chromium), and colors the optimum, suboptimum and even-less-good values with the success, warning and error tokens; a meter without regions takes the current color (`data-color`, primary by default). `data-size` (small, medium, large) changes the thickness. Under forced colors the track gets a border and every region is `Highlight`. The tokens it reads:

| Token | Default (light) | Applies to |
| --- | --- | --- |
| `--slean-radius-full` | `9999px` | border-radius |
| `--slean-muted` | `var(--slean-neutral-3)` | background |
| `--slean-success-main` | `oklch(49% 0.13 150)` | background |
| `--slean-warning-main` | `oklch(52% 0.125 55)` | background |
| `--slean-error-main` | `oklch(52% 0.19 25)` | background |
| `--slean-color-main` | `var(--slean-primary-main)` | background |

State selectors the stylesheet targets, all from the platform or ARIA: `:-moz-meter-sub-optimum`, `:-moz-meter-sub-sub-optimum`, `::-moz-meter-bar`, `::-webkit-meter-bar`, `::-webkit-meter-even-less-good-value`, `::-webkit-meter-inner-element`, `::-webkit-meter-optimum-value`, `::-webkit-meter-suboptimum-value`, `[high]`, `[low]`.

Variant attributes: `data-size` (`small`, `large`).

## Under the hood

Runtime and size

- **Tier**: 0 · Native
- **Behavior JS**: 0 B brotli · 0 B gzip · `fixtures/results.json (native-only)` · [method](https://svelte-lean.vercel.app/performance/methodology.md)
- **Platform features**: `<meter>`, `low`, `high`, `optimum`, `::-webkit-meter-optimum-value`
- **Shared listeners**: none
- **Per-instance listeners**: none
- **Lazy state**: none
- **Native base**: `<meter min max low high optimum value>`

Tier 0: the meter has no behavior module, the Vite plugin maps `meter` to no module, and the page ships no Svelte Lean JavaScript for it.

Why this implementation

A meter is a measurement with a judgement: storage that is fine, tight or full. The platform already makes the judgement: `low`, `high` and `optimum` split the range and the browser decides which region the value falls in, with no script and no comparison in the template.

What the platform leaves to authors is the look. `meter.css` sets `appearance: none`, draws the track and gives each region a status token through the vendor pseudo-elements, so the color follows the browser’s decision.

### The browser owns

- the value and the range
- placing the value in the optimum, suboptimum or even-less-good region
- the meter role with its name and value
- the direction of the fill under RTL

### Svelte Lean owns

- meter.css: the track, a status token per region, the current color (data-color, primary by default) when there are no regions
- the sizes
- the contract
- documentation

Accessibility

- The browser exposes the `meter` role with the value; a name comes from `<label for>`, `aria-labelledby` or `aria-label`.
- `aria-valuetext` gives the value a text form when the number alone says little (a strength word for a score).
- The region is shown by color only; the value in text carries the meaning for everyone.
- A meter is not a progress indicator: a task that completes is a progress.

Platform features

| Feature | Baseline | Outside the target |
| --- | --- | --- |
| `<meter> with low, high, optimum` | Widely available | Not applicable |
| `appearance: none` | Baseline 2022 | The platform’s own gauge |
| `::-webkit-meter-*-value, ::-moz-meter-bar` | Not standardized; Chromium and WebKit use the first, Firefox the second | Not applicable |

Without JavaScript

Fully functional: the rendered value and its region are visible.

Server rendering and hydration

The attributes render the value and the thresholds; the browser computes the region on the first paint.

Nothing is attached. The meter rendered on the server is the element the application updates after hydration.

Compatibility

The element and its thresholds are widely available; `appearance: none` is Baseline 2022. The region pseudo-elements are not standardized: Chromium and WebKit draw the `::-webkit-meter-*-value` elements, Firefox draws `::-moz-meter-bar` and exposes the region as a pseudo-class.

Tests and source

- `apps/playground/tests/primitives/feedback-navigation.spec.ts` Playwright: roles, names and states in the accessibility tree, keyboard, RTL, reduced motion, axe
- `packages/styles/tests` static checks of the stylesheet (layers, tokens, specificity, dark parity)

- `packages/primitives/src/meter/contract.md` the contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behavior
- `packages/primitives/src/meter/contract.ts` typed constants: name, tier, base, parts, options, events
- `packages/styles/css/meter.css` the optional stylesheet
