# Meter

Reports a measurement inside a known range, like disk usage or a score.

Meter reports where a measurement sits inside a range that is known up front:
disk usage, a password score, how much of a budget is left. Useful whenever the
number is a level rather than the state of a task; for something that starts,
runs and finishes, use [`Progress`](/components/progress).

::component-preview{name="MeterPreview"}
```vue
<template>
  <div class="flex w-64 flex-col gap-5">
    <Meter.Root
      v-slot="{ percentage }"
      :value="used"
      :options="{ max: total }"
      class="flex flex-col gap-2"
    >
      <div
        class="type-surface-code text-surface-muted flex items-baseline justify-between"
      >
        <Meter.Label>Storage</Meter.Label>
        <Meter.Value class="tabular-nums" />
      </div>

      <Meter.Track :class="track">
        <Meter.Indicator :class="indicator" />
      </Meter.Track>

      <p class="type-surface-caption text-surface-muted tabular-nums">
        {{ used }} GB of {{ total }} GB, {{ Math.round(percentage) }}% full
      </p>
    </Meter.Root>

    <div class="flex gap-2">
      <Button :class="button" @click="step(-64)">Free 64 GB</Button>
      <Button :class="button" @click="step(64)">Add 64 GB</Button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Button, Meter } from '@maas/mirror/vue'

const total = 512
const used = ref(384)

function step(amount: number) {
  used.value = Math.min(total, Math.max(0, used.value + amount))
}

const track = 'rounded-component-round bg-primary-subtle h-2.5 overflow-hidden'

const indicator = [
  'bg-primary-solid h-full',
  'transition-all duration-300 ease-linear',
].join(' ')

const button = [
  'rounded-component-md border-surface border px-2.5 py-1',
  'type-component-xs text-surface-muted hover:text-surface',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
].join(' ')
</script>
```
::

## Usage guidelines

- A meter has no implicit name, so render a `Meter.Label` or give the root an
  `aria-label`. The label part wires `aria-labelledby` for you.
- The value is always a number, so leave the meter unrendered until there is
  something to report.
- Only the percentage and the formatted string are clamped, and `aria-valuenow`
  reports the clamped value, so clamp before you pass it in where the source
  can overshoot.
- `options.format` resolves against the runtime locale unless you pin one with
  `options.locale`. If the server and the client can differ, pass both, or fix
  the digits with `minimumFractionDigits` and `maximumFractionDigits`.

## Anatomy

Assemble the parts, in whatever order the layout needs.

::component-anatomy
---
parts:
  - name: Meter.Root
    required: true
    description: 'Renders a <div role="meter">. Owns the value and writes the percentage.'
    children:
      - name: Meter.Label
        description: Renders a <span> that names the meter through aria-labelledby.
      - name: Meter.Track
        description: 'Renders a <div>. Structural: the full length of the bar.'
        children:
          - name: Meter.Indicator
            description: Renders a <div>, sized to the current percentage.
      - name: Meter.Value
        description: Renders a <span> with the formatted value.
---
::

```vue
<script setup lang="ts">
import { Meter } from '@maas/mirror/vue'
</script>

<template>
  <Meter.Root :value="used" :options="{ max: total }">
    <Meter.Label>Storage</Meter.Label>

    <Meter.Track>
      <Meter.Indicator />
    </Meter.Track>

    <Meter.Value />
  </Meter.Root>
</template>
```

## Examples

### Naming the meter

`Meter.Label` renders a `<span>`, takes an ID from the root and points the
root’s `aria-labelledby` at itself.

```vue
<template>
  <Meter.Root :value="score" :options="{ max: 4 }">
    <Meter.Label>Password strength</Meter.Label>

    <Meter.Track>
      <Meter.Indicator />
    </Meter.Track>
  </Meter.Root>
</template>
```

Leave the label out where the surrounding copy already names the meter, and
give the root an `aria-label` instead.

```vue
<template>
  <Meter.Root
    :value="score"
    :options="{ max: 4 }"
    aria-label="Password strength"
  >
    <Meter.Track>
      <Meter.Indicator />
    </Meter.Track>
  </Meter.Root>
</template>
```

### Sizing the indicator

`Meter.Root` writes the percentage to `--mme-percentage`, a unitless number
between `0` and `100`, and the CSS we ship turns it into the indicator’s width.

```css
.mirror-meter-indicator[data-orientation='horizontal'] {
  block-size: 100%;
  inline-size: calc(var(--mme-percentage, 0) * 1%);
}
```

The property sits on the root, so anything inside the component can read it,
including a tick mark, a moving label or a conic-gradient ring.

```css
.ring {
  background: conic-gradient(
    var(--app-color-primary-bg-solid) calc(var(--mme-percentage, 0) * 1%),
    var(--app-color-surface-bg-higher) 0
  );
}
```

### Vertical meters

Set `options.orientation` to `vertical` and every part says so through
`data-orientation`, while the indicator fills along the block axis instead.

```vue
<template>
  <Meter.Root :value="load" :options="{ orientation: 'vertical' }">
    <Meter.Track class="track">
      <Meter.Indicator />
    </Meter.Track>
  </Meter.Root>
</template>

<style>
.track {
  block-size: 8rem;
  display: flex;
  align-items: flex-end;
}
</style>
```

The track keeps its thickness on the free axis, so
`--mirror-meter-track-size` becomes an inline size rather than a block one.

### Formatting the value

`options.format` drives both `Meter.Value` and `aria-valuetext`, and defaults
to `{ style: 'percent' }`, which formats the percentage rather than the value.

```vue
<template>
  <Meter.Root :value="384" :options="{ max: 512 }" aria-label="Storage">
    <Meter.Value />
    <!-- 75% -->
  </Meter.Root>
</template>
```

Any other style formats the clamped value instead, which is how you report raw
units rather than a share of the whole.

```vue
<template>
  <Meter.Root
    :value="384"
    :options="{ max: 512, format: { style: 'unit', unit: 'gigabyte' } }"
    aria-label="Storage"
  >
    <Meter.Value />
    <!-- 384 GB -->
  </Meter.Root>
</template>
```

Pass `options.locale` where the runtime locale is not the one you want, and
both the rendered value and `aria-valuetext` follow it.

```vue
<template>
  <Meter.Root
    :value="384"
    :options="{
      max: 512,
      locale: 'de-DE',
      format: { style: 'unit', unit: 'gigabyte' },
    }"
    aria-label="Speicher"
  >
    <Meter.Value />
    <!-- 384 GB, with German separators -->
  </Meter.Root>
</template>
```

Take the slot to put the formatted string in markup of your own.

```vue
<template>
  <Meter.Value v-slot="{ formatted, percentage }">
    {{ formatted }} · {{ Math.round(percentage) }} of 100
  </Meter.Value>
</template>
```

### Overriding the announcement

`options.getAriaValueText` receives the formatted string and the raw value, and
what it returns replaces `aria-valuetext` entirely.

```vue
<template>
  <Meter.Root
    :value="used"
    :options="{
      max: total,
      getAriaValueText: (formatted, value) => `${value} of ${total} GB used`,
    }"
  >
    <Meter.Label>Storage</Meter.Label>

    <Meter.Track>
      <Meter.Indicator />
    </Meter.Track>
  </Meter.Root>
</template>
```

### Driving it from elsewhere

Give the root an `id` and `useMirrorMeter(id)` reaches it from anywhere in the
app, which is how a meter in a sidebar follows a number computed in a store.

```vue
<script setup lang="ts">
import { useMirrorMeter } from '@maas/mirror/vue'

const { percentage, setValue } = useMirrorMeter('storage')
</script>

<template>
  <Button @click="setValue(512)">Fill up</Button>

  <Meter.Root id="storage" :options="{ max: 512 }">
    <Meter.Label>Storage</Meter.Label>

    <Meter.Track>
      <Meter.Indicator />
    </Meter.Track>
  </Meter.Root>
</template>
```

## API reference

**Module.** A bundled `options` object, and `useMirrorMeter(id)` as the
programmatic API.

### `Meter.Root`

Renders a `<div role="meter">`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: 'The instance ID, for [`useMirrorMeter`](#driving-it-from-elsewhere).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: value
        description: The measurement.
      - label: number
      - label: '0'
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: MeterOptions
      - label: see below
        plaintext: true
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: min
        description: Lower bound of the range.
      - label: number
      - label: '0'
  - items:
      - label: max
        description: Upper bound of the range.
      - label: number
      - label: '100'
  - items:
      - label: orientation
        description: 'Axis the meter [fills along](#vertical-meters).'
      - label: '''horizontal'' | ''vertical'''
      - label: '''horizontal'''
  - items:
      - label: locale
        description: 'Locale for [the formatting](#formatting-the-value).'
      - label: Intl.LocalesArgument
      - label: undefined
  - items:
      - label: format
        description: 'Formatting for `Meter.Value` and [`aria-valuetext`](#formatting-the-value).'
      - label: Intl.NumberFormatOptions
      - label: '{ style: ''percent'' }'
  - items:
      - label: getAriaValueText
        description: '[Replaces `aria-valuetext`](#overriding-the-announcement).'
      - label: '(formatted: string, value: number) => string'
        escape: true
      - label: undefined
---
::

#### Emits

None. `Meter` is a readout.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The raw value, unclamped.
      - label: number
  - items:
      - label: percentage
        description: The clamped percentage, `0` to `100`.
      - label: number
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: '''horizontal'' | ''vertical'''
  - items:
      - label: formatted
        description: The formatted value.
      - label: string
---
::

### `Meter.Label`

Renders a `<span>` carrying the ID the root points `aria-labelledby` at. No
props of its own beyond `id`.

### `Meter.Track`

Renders a `<div>`, the full length of the bar and the box the indicator is
measured against. No props of its own beyond `id`.

### `Meter.Indicator`

Renders a `<div>`, sized to `calc(var(--mme-percentage, 0) * 1%)` on the axis
the orientation fills and `100%` on the other. No props of its own beyond `id`.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: percentage
        description: The clamped percentage, `0` to `100`.
      - label: number
  - items:
      - label: orientation
        description: Mirrors `data-orientation`.
      - label: '''horizontal'' | ''vertical'''
---
::

### `Meter.Value`

Renders a `<span>` with the formatted value. It is not hidden from screen
readers.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The raw value, unclamped.
      - label: number
  - items:
      - label: percentage
        description: The clamped percentage, `0` to `100`.
      - label: number
  - items:
      - label: formatted
        description: The formatted value.
      - label: string
---
::

### Composable

`useMirrorMeter(id)` reaches a meter from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: value
        description: The raw value.
      - label: ComputedRef<number>
        escape: true
  - items:
      - label: percentage
        description: The clamped percentage.
      - label: ComputedRef<number>
        escape: true
  - items:
      - label: formatted
        description: The formatted value.
      - label: ComputedRef<string>
        escape: true
  - items:
      - label: setValue
        description: Sets the value.
      - label: '(next: number) => void'
        escape: true
---
::

### Data attributes

Every part writes the same values, so any of them can be styled from the state
without a wrapper class.

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_meter_context
        description: '`Meter.Label`, `Meter.Track`, `Meter.Indicator` or `Meter.Value` rendered outside `Meter.Root`.'
  - items:
      - label: invalid_meter_range
        description: '`min` is greater than or equal to `max`. Checked on every change, not only on mount.'
---
::

## Accessibility

`Meter.Root` renders a `role="meter"` with `aria-valuemin`, `aria-valuemax`,
`aria-valuenow` and `aria-valuetext`, and takes no focus and no keyboard
interaction.

Assistive technology treats a meter as a gauge, so anything the user can change
belongs in a [`Slider`](/components/slider), and anything with a beginning and
an end belongs in [`Progress`](/components/progress).

`Meter.Value` stays readable by assistive technology, because a reader that
lands on the meter itself hears `aria-valuetext` and a reader moving through the
text hears the value part.

`aria-labelledby` is written once `Meter.Label` has registered, which happens on
the tick after the root renders. Where a name has to be present in the first
paint, such as server-rendered markup, pass an `aria-label` as well.
