# Meter

## Summary

`Meter` shows a bounded quantitative value, typically without the temporal progress semantics of a progress bar. It is the descriptive-measurement bar when the UI should read as level, score, capacity, or quality rather than task completion.

## Imports

```tsx
import {
  Meter,
  MeterTrack,
  type MeterProps,
  type MeterSize,
  type MeterVariant,
} from '@vega-ui/react';
```

## Exported Types

- `MeterProps`
- `MeterTrackProps`
- `MeterSize`
- `MeterVariant`

## Minimal Composition

`max` defaults to `1`, so a fraction like `0.72` needs no `max`; pass `max` for any other scale (`value={36} max={50}`). The meter has no visible label of its own: give it `aria-label` or `aria-labelledby`.

```tsx
import { Meter, MeterTrack } from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <Meter value={0.72} valueText='72%' aria-label='Disk usage' fullWidth>
      <MeterTrack />
    </Meter>
  );
}
```

## Required Parts

- `Meter`: root meter surface
- `MeterTrack`: visible measured-fill track

## Optional Parts

- No extra public parts beyond root and track

## Composition Order

1. `Meter`
2. `MeterTrack`

## Variants

- Sizes: `sm`, `md`, `lg`
- Variants: `primary`, `secondary`
- States: neutral, good, warn, bad via thresholds

## Edge Cases

- `Meter` is for bounded measurement, not async completion.
- Threshold props like `low`, `high`, and `optimum` can shift the semantic color state.
- Surrounding copy should explain the measured value and range.
- If the measurement is actually composed of several meaningful parts, `MeterStack` is usually clearer than one continuous bar.

## Common Mistakes

- Using `Meter` for loading or async work.
- Showing a value without explaining the range or meaning.
- Ignoring threshold semantics when they matter to interpretation.

## Examples

### Basic

#### Basic: storage usage with a visible summary

A meter on a real scale: `max` is the plan limit, so `value` is passed in gigabytes, not as a fraction. The visible heading names the meter through `aria-labelledby`, and `valueText` repeats the human-readable summary for screen readers instead of a bare number.

```tsx
import { Meter, MeterTrack, Text } from '@vega-ui/react';
import { useId } from 'react';

export default function StorageUsage() {
  const labelId = useId();
  const usedGb = 36.2;
  const limitGb = 50;
  const summary = `${usedGb} GB of ${limitGb} GB used`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 'var(--spacing-8)' }}>
        <Text id={labelId} size={2} fontWeight={500}>Cloud storage</Text>
        <Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>{summary}</Text>
      </div>
      <Meter value={usedGb} max={limitGb} valueText={summary} aria-labelledby={labelId} fullWidth>
        <MeterTrack />
      </Meter>
    </div>
  );
}
```

### Controlled/Stateful

#### Controlled/Stateful: battery level with threshold zones

The parent owns the level and passes it to the meter together with `low`, `high` and `optimum`. The track turns red, amber or green on its own, while the same thresholds drive a text status linked through `aria-describedby`, so the meaning never relies on color.

```tsx
import { Button, Meter, MeterTrack, Text } from '@vega-ui/react';
import { useId, useState } from 'react';

export default function BatteryLevel() {
  const labelId = useId();
  const statusId = useId();
  const [level, setLevel] = useState(64);

  let status = 'Battery is fine';
  if (level < 20) {
    status = 'Battery is low — connect the charger';
  } else if (level > 80) {
    status = 'Battery is almost full';
  }

  const discharge = () => setLevel(Math.max(level - 15, 0));
  const charge = () => setLevel(Math.min(level + 15, 100));

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 360 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 'var(--spacing-8)' }}>
          <Text id={labelId} size={2} fontWeight={500}>Headphones battery</Text>
          <Text size={2} style={{ fontVariantNumeric: 'tabular-nums' }}>{level}%</Text>
        </div>
        <Meter
          value={level}
          max={100}
          low={20}
          high={80}
          optimum={100}
          valueText={`${level}%`}
          aria-labelledby={labelId}
          aria-describedby={statusId}
          fullWidth
        >
          <MeterTrack />
        </Meter>
        <Text id={statusId} size={2} style={{ color: 'var(--label-secondary)' }}>{status}</Text>
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-4)' }}>
        <Button size='sm' variant='secondary' appearance='ghost' onClick={discharge} disabled={level === 0}>
          Listen for an hour
        </Button>
        <Button size='sm' variant='secondary' appearance='ghost' onClick={charge} disabled={level === 100}>
          Charge for 15 minutes
        </Button>
      </div>
    </div>
  );
}
```

### Form/Integration

#### Form/Integration: character limit under a textarea

The meter follows the length of a `TextArea` inside a real form. `optimum={0}` makes short text the good zone, so the bar turns amber near the limit and red at it; `maxLength` enforces the hard cap and the remaining count is linked to the field. The submit handler reads the value with `FormData`, and reset clears the state.

```tsx
import { Button, HelperText, Label, Meter, MeterTrack, TextArea } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

export default function BioLengthMeter() {
  const fieldId = useId();
  const counterId = useId();
  const limit = 160;
  const [bio, setBio] = useState('Product designer in Lisbon. I write about design systems and accessible forms.');
  const [saved, setSaved] = useState<string | null>(null);

  const left = limit - bio.length;

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const value = String(data.get('bio'));
    setSaved(`Saved, ${value.length} characters`);
  };

  const handleReset = () => {
    setBio('');
    setSaved(null);
  };

  return (
    <form onSubmit={handleSubmit} onReset={handleReset} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={fieldId}>Profile bio</Label>
        <TextArea
          id={fieldId}
          name='bio'
          rows={3}
          maxLength={limit}
          required
          value={bio}
          onChange={(event) => setBio(event.target.value)}
          aria-describedby={counterId}
          fullWidth
        />
        <Meter
          value={bio.length}
          max={limit}
          low={120}
          high={150}
          optimum={0}
          valueText={`${bio.length} of ${limit} characters`}
          aria-label='Bio length'
          size='sm'
          fullWidth
        >
          <MeterTrack />
        </Meter>
        <HelperText id={counterId} style={{ fontVariantNumeric: 'tabular-nums' }}>{left} characters left</HelperText>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
        <Button type='submit' size='sm'>Save</Button>
        <Button type='reset' size='sm' variant='secondary' appearance='ghost'>Clear</Button>
        <HelperText role='status' style={{ marginInlineStart: 'var(--spacing-4)' }}>{saved}</HelperText>
      </div>
    </form>
  );
}
```

### Layout/Overlay

#### Layout/Overlay: plan usage card

Several meters in a card, one list item each, every one labelled by its own row heading. Lower is better here (`optimum={0}`), so a resource close to its limit stands out without extra code.

```tsx
import { Card, Heading, Meter, MeterTrack, Text } from '@vega-ui/react';
import { useId } from 'react';

const usage = [
  { id: 'seats', label: 'Seats', used: 18, limit: 20, unit: 'seats' },
  { id: 'storage', label: 'Storage', used: 412, limit: 500, unit: 'GB' },
  { id: 'minutes', label: 'Build minutes', used: 1240, limit: 3000, unit: 'min' },
];

export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];

export default function MeterInsideACard({ locale = 'en-US' }: { locale?: string }) {
  const prefix = useId();
  const format = new Intl.NumberFormat(locale);

  return (
    <Card size='sm' style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <Heading as='h3' size={4} fontWeight={600}>Pro plan usage</Heading>
      <ul style={{ display: 'grid', gap: 'var(--spacing-8)', margin: 0, padding: 0, listStyle: 'none' }}>
        {usage.map((item) => {
          const labelId = `${prefix}-${item.id}`;
          const summary = `${format.format(item.used)} of ${format.format(item.limit)} ${item.unit}`;

          return (
            <li key={item.id} style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 'var(--spacing-8)' }}>
                <Text id={labelId} size={2}>{item.label}</Text>
                <Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>{summary}</Text>
              </div>
              <Meter
                value={item.used}
                max={item.limit}
                low={item.limit * 0.75}
                high={item.limit * 0.9}
                optimum={0}
                valueText={summary}
                aria-labelledby={labelId}
                size='sm'
                fullWidth
              >
                <MeterTrack />
              </Meter>
            </li>
          );
        })}
      </ul>
    </Card>
  );
}
```

### Error

#### Error: quota exceeded

The meter clamps the fill at `max`, so an overflow cannot be seen in the bar alone. When usage passes the limit, the example renders an error `HelperText`, links it to the meter with `aria-describedby` and says exactly how far over the limit the account is.

```tsx
import { Button, HelperText, Meter, MeterTrack, Text } from '@vega-ui/react';
import { useId, useState } from 'react';

export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];

export default function QuotaExceeded({ locale = 'en-US' }: { locale?: string }) {
  const labelId = useId();
  const errorId = useId();
  const limit = 10000;
  const [requests, setRequests] = useState(8700);

  const format = new Intl.NumberFormat(locale);
  const exceeded = requests > limit;
  const summary = `${format.format(requests)} of ${format.format(limit)} requests`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 'var(--spacing-8)' }}>
          <Text id={labelId} size={2} fontWeight={500}>API requests</Text>
          <Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>{summary}</Text>
        </div>
        <Meter
          value={requests}
          max={limit}
          low={limit * 0.8}
          high={limit}
          optimum={0}
          valueText={summary}
          aria-labelledby={labelId}
          aria-describedby={exceeded ? errorId : undefined}
          fullWidth
        >
          <MeterTrack />
        </Meter>
        {exceeded && (
          <HelperText id={errorId} error style={{ fontVariantNumeric: 'tabular-nums' }}>
            Limit exceeded by {format.format(requests - limit)} requests. New requests are rejected until 1 December.
          </HelperText>
        )}
      </div>
      <div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
        <Button size='sm' variant='secondary' appearance='ghost' onClick={() => setRequests(requests + 900)}>
          Run nightly sync
        </Button>
        <Button size='sm' variant='secondary' appearance='ghost' onClick={() => setRequests(8700)}>
          Reset
        </Button>
      </div>
    </div>
  );
}
```

### Edge

#### Edge: non-zero minimum and an optimum in the middle

A temperature scale from 10 to 35 °C with the comfortable zone in the middle: values below `low` or above `high` are both bad. `min` shifts the start of the track, and the verdict is printed next to each value.

```tsx
import { Meter, MeterTrack, Text } from '@vega-ui/react';
import { useId } from 'react';

const rooms = [
  { id: 'living', name: 'Living room', celsius: 21.5 },
  { id: 'bedroom', name: 'Bedroom', celsius: 18.2 },
  { id: 'server', name: 'Server closet', celsius: 29.8 },
];

export default function RoomTemperature() {
  const prefix = useId();

  return (
    <ul style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 360, margin: 0, padding: 0, listStyle: 'none' }}>
      {rooms.map((room) => {
        const labelId = `${prefix}-${room.id}`;
        let verdict = 'Comfortable';
        if (room.celsius < 19) {
          verdict = 'Too cold';
        } else if (room.celsius > 24) {
          verdict = 'Too warm';
        }

        return (
          <li key={room.id} style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 'var(--spacing-8)' }}>
              <Text id={labelId} size={2}>{room.name}</Text>
              <Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>{room.celsius} °C · {verdict}</Text>
            </div>
            <Meter
              value={room.celsius}
              min={10}
              max={35}
              low={19}
              high={24}
              optimum={21.5}
              valueText={`${room.celsius} °C, ${verdict.toLowerCase()}`}
              aria-labelledby={labelId}
              fullWidth
            >
              <MeterTrack />
            </Meter>
          </li>
        );
      })}
    </ul>
  );
}
```

## Anatomy

### Overview

`Meter` is a [compound component](https://vega-ui.dev/docs/glossary/#compound-component) because the root meter role and the visible track share one bounded measurement model.

### Required Parts

#### `Meter`

Required. Owns range values, thresholds, variant, size, and accessibility attributes.

#### `MeterTrack`

Required. Renders the visible measured-fill amount.

### Optional Parts

`Meter` does not expose additional public parts beyond the root and track.

### Composition Order

1. `Meter`
2. `MeterTrack`

### Valid Composition Patterns

```tsx
import { Meter, MeterTrack } from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <Meter value={0.72} valueText='72%' aria-label='Disk usage' fullWidth>
      <MeterTrack />
    </Meter>
  );
}
```

### Invalid Composition Patterns

#### Meter used for ongoing async completion

If the value represents temporal completion, `Progress` is the better pattern.

#### Threshold-based colors without explaining what the range means

Users may see semantic color shifts but not understand the measurement.
