# Meter (/ui/components/react/meter)



<Playground component="meter" />

## Usage [#usage]

```tsx
import { Meter, MeterLabel, MeterValue, MeterProgress } from '@appica/ui-react/meter'
```

```tsx
<Meter value={64}>
  <MeterLabel>Storage</MeterLabel>
  <MeterValue />
  <MeterProgress />
</Meter>
```

A `Meter` displays a **static measurement** inside a known range — disk usage, a score, remaining battery — as opposed to [`Progress`](/ui/components/react/progress), which tracks a task moving toward completion. Compose it from parts: `MeterLabel` names the reading, `MeterValue` prints the formatted number, and `MeterProgress` draws the track and fill. The root lays them out on a grid — label top-left, value top-right, track spanning underneath — so you can drop in only the parts you need.

By default the fill uses the primary accent. Pass the `low`, `high`, and `optimum` thresholds and the indicator recolors itself to signal whether the current reading is good, borderline, or bad — green, amber, or red.

## Examples [#examples]

### Default [#default]

A label, a formatted value, and a track. `value` is read against the default `0–100` range, and `MeterValue` renders it as a percentage out of the box.

```tsx
import { Meter, MeterLabel, MeterValue, MeterProgress } from '@appica/ui-react/meter'

export default function MeterDefault() {
  return (
    <Meter value={64} className="w-72 max-w-full">
      <MeterLabel>Storage</MeterLabel>
      <MeterValue />
      <MeterProgress />
    </Meter>
  )
}
```

### Status thresholds [#status-thresholds]

Pass `low`, `high`, and `optimum` and the indicator colors itself by zone: **optimum** (green) when the value sits on the same side as `optimum`, **suboptimum** (orange) one zone away, and **invalid** (red) at the far end. Here `optimum={10}` marks low usage as healthy, so a high reading turns red.

```tsx
import { Meter, MeterLabel, MeterValue, MeterProgress } from '@appica/ui-react/meter'

const USAGE = [
  { label: 'Operations', value: 32 },
  { label: 'Marketing', value: 64 },
  { label: 'Infrastructure', value: 94 },
]

export default function MeterStatus() {
  return (
    <div className="flex w-72 max-w-full flex-col gap-5">
      {USAGE.map((item) => (
        <Meter key={item.label} value={item.value} low={40} high={75} optimum={10}>
          <MeterLabel>{item.label}</MeterLabel>
          <MeterValue />
          <MeterProgress />
        </Meter>
      ))}
    </div>
  )
}
```

### Formatting the value [#formatting-the-value]

The root's `format` takes [`Intl.NumberFormatOptions`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options) — render the value as currency, a unit, and so on. For full control, pass a function as `MeterValue`'s children to wrap the formatted string in your own text.

```tsx
'use client'

import { Meter, MeterLabel, MeterValue, MeterProgress } from '@appica/ui-react/meter'

export default function MeterFormatting() {
  return (
    <div className="flex w-72 max-w-full flex-col gap-6">
      <Meter value={1280} max={2000} format={{ style: 'currency', currency: 'USD' }} low={1000} high={1800} optimum={0}>
        <MeterLabel>Monthly budget</MeterLabel>
        <MeterValue />
        <MeterProgress />
      </Meter>

      <Meter value={3.4} max={8} format={{ style: 'unit', unit: 'gigabyte', maximumFractionDigits: 1 }}>
        <MeterLabel>Backup size</MeterLabel>
        <MeterValue>{(formatted) => `${formatted} of 8 GB`}</MeterValue>
        <MeterProgress />
      </Meter>
    </div>
  )
}
```

### Password strength (controlled) [#password-strength-controlled]

A real-world meter: score the input as the user types, map the score to a custom range (`max={4}`), and use the thresholds to drive the red → amber → green color. The `MeterValue` children print a word ("Fair", "Strong") instead of a number.

```tsx
'use client'

import * as React from 'react'
import { Meter, MeterValue, MeterProgress } from '@appica/ui-react/meter'
import { Input } from '@appica/ui-react/input'
import { Lock } from '@appica/icons-react'

const LEVELS = ['Too weak', 'Weak', 'Fair', 'Good', 'Strong'] as const

function scorePassword(value: string) {
  if (value.length < 3) return 0
  let score = 1
  if (value.length >= 10) score++
  if (/[A-Z]/.test(value) && /[a-z]/.test(value)) score++
  if (/[0-9]/.test(value) || /[^A-Za-z0-9]/.test(value)) score++
  return score
}

export default function MeterStrength() {
  const [password, setPassword] = React.useState('')
  const score = scorePassword(password)

  return (
    <div className="flex w-72 max-w-full flex-col gap-2">
      <Input
        startSlot={<Lock />}
        type="password"
        autoComplete="new-password"
        value={password}
        onChange={(event) => setPassword(event.target.value)}
        placeholder="Choose a password"
        aria-label="Password"
      />
      <Meter
        value={score}
        max={4}
        low={2}
        high={2}
        optimum={4}
        aria-label="Password strength"
        statusClassNames={{
          invalid: 'bg-error-emphasis',
          suboptimum: 'bg-warning-emphasis',
          optimum: 'bg-success-emphasis',
        }}
      >
        <MeterValue className="text-foreground-muted text-xs">{() => LEVELS[score]}</MeterValue>
        <MeterProgress />
      </Meter>
    </div>
  )
}
```

### Minimal [#minimal]

Drop `MeterValue` (and even `MeterLabel`) to render a bare track — handy for compact lists where the label alone carries the meaning. Restyle the track height and label with `className`.

```tsx
import { Meter, MeterLabel, MeterProgress } from '@appica/ui-react/meter'

const SKILLS = [
  { label: 'TypeScript', value: 90 },
  { label: 'React', value: 82 },
  { label: 'CSS', value: 70 },
  { label: 'Rust', value: 38 },
]

export default function MeterMinimal() {
  return (
    <div className="flex w-64 max-w-full flex-col gap-3">
      {SKILLS.map((skill) => (
        <Meter key={skill.label} value={skill.value} className="gap-1">
          <MeterLabel className="text-foreground text-xs font-normal">{skill.label}</MeterLabel>
          <MeterProgress className="h-1" />
        </Meter>
      ))}
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

Under RTL the label and value swap sides and the track fills from the right. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="meter" />

## API reference [#api-reference]

`Meter` wraps [Base UI's Meter](https://base-ui.com/react/components/meter). Each part forwards `ref` and its remaining native attributes to the matching Base UI primitive — the tables below list the Appica additions and the most common props. When any threshold is set, the root exposes `data-status` (`optimum` | `suboptimum` | `invalid`) for styling, and the indicator carries `data-slot="meter-indicator"` with the threshold-derived background class.

### Meter [#meter]

The root. Owns the `value`, the range, and the threshold logic, and provides the resolved indicator color to `MeterProgress`. Renders a `<div role="meter">`.

| Prop               | <ColMinWidth width="220">Type</ColMinWidth>                    | Default | <ColMinWidth width="220">Description</ColMinWidth>                                               |
| ------------------ | -------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------ |
| `value`            | `number`                                                       | —       | **Required.** The current value.                                                                 |
| `min`              | `number`                                                       | `0`     | Lower bound of the range.                                                                        |
| `max`              | `number`                                                       | `100`   | Upper bound of the range.                                                                        |
| `low`              | `number`                                                       | —       | Upper edge of the "low" zone. Enables status coloring.                                           |
| `high`             | `number`                                                       | —       | Lower edge of the "high" zone. Enables status coloring.                                          |
| `optimum`          | `number`                                                       | —       | The ideal value; the zone it lands in becomes the green "optimum" zone. Enables status coloring. |
| `statusClassNames` | <Code>{`{ optimum?, suboptimum?, invalid?, default? }`}</Code> | —       | Override the indicator background class per status (and the no-threshold `default`).             |
| `format`           | `Intl.NumberFormatOptions`                                     | —       | Formatting passed to `Intl.NumberFormat` for the displayed value.                                |
| `locale`           | `Intl.LocalesArgument`                                         | —       | Locale used by `Intl.NumberFormat`.                                                              |
| `getAriaValueText` | <Code>(formatted: string, value: number) => string</Code>      | —       | Build the human-readable `aria-valuetext`.                                                       |
| `render`           | <Code>ReactElement \| ((props, state) => ReactElement)</Code>  | —       | Replace the rendered element, or compose it with another component.                              |
| `className`        | <Code>string \| ((state) => string)</Code>                     | —       | Extra classes, merged via `tailwind-merge`. Override the default grid layout here.               |
| `style`            | <Code>CSSProperties \| ((state) => CSSProperties)</Code>       | —       | Inline styles, optionally derived from state.                                                    |

### MeterLabel [#meterlabel]

Names the meter and is wired to the root via `aria-labelledby`. Renders a `<span>`.

| Prop        | <ColMinWidth width="220">Type</ColMinWidth>                   | Default | Description                                   |
| ----------- | ------------------------------------------------------------- | ------- | --------------------------------------------- |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace or compose the rendered element.      |
| `className` | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.   |
| `style`     | <Code>CSSProperties \| ((state) => CSSProperties)</Code>      | —       | Inline styles, optionally derived from state. |

### MeterValue [#metervalue]

Prints the formatted value. Renders a `<span>`.

| Prop        | <ColMinWidth width="220">Type</ColMinWidth>                            | Default | <ColMinWidth width="220">Description</ColMinWidth>                           |
| ----------- | ---------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------- |
| `children`  | <Code>((formatted: string, value: number) => ReactNode) \| null</Code> | —       | Render function to fully customize the displayed text. Omit for the default. |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code>          | —       | Replace or compose the rendered element.                                     |
| `className` | <Code>string \| ((state) => string)</Code>                             | —       | Extra classes, merged via `tailwind-merge`.                                  |
| `style`     | <Code>CSSProperties \| ((state) => CSSProperties)</Code>               | —       | Inline styles, optionally derived from state.                                |

### MeterProgress [#meterprogress]

The track and fill. Wraps the `Track` and renders the `Indicator` inside it, reading the resolved color from the root. Renders a `<div>`.

| Prop        | <ColMinWidth width="220">Type</ColMinWidth>                   | Default | Description                                              |
| ----------- | ------------------------------------------------------------- | ------- | -------------------------------------------------------- |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace or compose the track element.                    |
| `className` | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes on the track, merged via `tailwind-merge`. |
| `style`     | <Code>CSSProperties \| ((state) => CSSProperties)</Code>      | —       | Inline styles, optionally derived from state.            |

## Accessibility [#accessibility]

* `Meter` renders `role="meter"` with `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` set from `value`, `min`, and `max`.
* Give every meter an accessible name — either a `MeterLabel` (wired automatically via `aria-labelledby`) or an `aria-label` on the root when you omit the visible label.
* `format` and `getAriaValueText` shape `aria-valuetext`, so screen readers announce "$1,280.00" rather than a bare number.
* Color alone shouldn't carry meaning — pair the status colors with the value or a word (as in the password-strength example) so the state is clear without color.
* The fill transition honours `prefers-reduced-motion`.
