Qure UI
Components

Meter

A measurement shown against a range — a reading, not a task.

PACS node storage
x
import { Meter, MeterLabel, MeterTrack, MeterValue } from '@/registry/qure/ui/meter'export default function MeterDemo() {  return (    <Meter className="w-72" value={68}>      <MeterLabel>PACS node storage</MeterLabel>      <MeterValue />      <MeterTrack />    </Meter>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/meter.json

Usage

import { Meter, MeterLabel, MeterTrack, MeterValue } from '@/components/ui/meter'
<Meter value={68}>
  <MeterLabel>PACS node storage</MeterLabel>
  <MeterValue />
  <MeterTrack />
</Meter>

value is required. The range is 0–100 unless you give it min and max.

Meter is not Progress

They look almost identical and mean opposite things, so this is the section worth reading.

MeterProgress
The number isa measurement taken nowhow much of a job is done
It moves becausethe thing being measured changedwork happened
Reaching the end isoften bad — a full disk, a dose at the limitthe goal
ARIA rolemeterprogressbar
A screen reader says"68 percent""68 percent, busy"

Storage used, beds occupied, dose against a notification level, an AI confidence score, agreement between two readers — all meters. None of them are heading anywhere, and dressing them as progress tells the reader to wait for something that is never going to happen.

The reverse mistake is quieter but worse: a genuine long-running job shown as a meter loses the busy state, and assistive technology stops telling the user that anything is in flight.

If the answer to "what happens when it reaches 100%?" is "nothing, it is just full", it is a meter.

Composition

<Meter>            {/* owns value, min, max and the aria plumbing */}
  <MeterLabel>     {/* the name — linked to the root by the primitive */}
  <MeterValue>     {/* the figure, formatted */}
  <MeterTrack>     {/* the bar; renders its own indicator */}
</Meter>

The parts are laid out on a two-column grid: label left, value right, track spanning both. Drop MeterLabel or MeterValue and the grid closes up, so a bare bar in a table row needs no extra wrapper — give the root an aria-label instead.

Tone

CT 1 — Siemens Force
x
CT 2 — GE Revolution
x
CT 3 — Philips iCT
x
import { Meter, MeterLabel, MeterTrack, MeterValue } from '@/registry/qure/ui/meter'/** * CTDIvol against the 20 mGy notification level for an adult head CT. The * tone is chosen by the caller from the reading, because the meter has no * idea which end of its range is the bad one. */const doses = [  { room: 'CT 1 — Siemens Force', mGy: 9.4, tone: 'success' as const },  { room: 'CT 2 — GE Revolution', mGy: 15.1, tone: 'attention' as const },  { room: 'CT 3 — Philips iCT', mGy: 22.6, tone: 'urgent' as const },]export default function MeterTone() {  return (    <div className="grid w-72 gap-5">      {doses.map((d) => (        <Meter key={d.room} tone={d.tone} value={d.mGy} max={30}>          <MeterLabel>{d.room}</MeterLabel>          <MeterValue>{(_, value) => `${value} mGy`}</MeterValue>          <MeterTrack />        </Meter>      ))}    </div>  )}

tone is yours to choose, not the component's. A meter has no idea whether 82% is good news, and one that guessed would be wrong the first time somebody measured a thing where low is bad. Here the CTDIvol readings are coloured against the 20 mGy notification level for an adult head CT.

Colour is never the only signal — the figure is always spelled out beside the bar.

A range that is not a percentage

Consolidation
x
Pleural effusion
x
import { Meter, MeterLabel, MeterTrack, MeterValue } from '@/registry/qure/ui/meter'/** * A qXR confidence score. The scale is 0–1, not 0–100, so `max` is 1 and * `format` turns the raw 0.84 into something a radiologist reads at a glance * without the component pretending the number was a percentage all along. */export default function MeterRange() {  return (    <div className="grid w-72 gap-5">      <Meter        value={0.84}        max={1}        format={{ style: 'percent', maximumFractionDigits: 0 }}        getAriaValueText={(formatted) => `${formatted} confidence, consolidation`}      >        <MeterLabel>Consolidation</MeterLabel>        <MeterValue />        <MeterTrack />      </Meter>      <Meter        tone="neutral"        value={0.62}        max={1}        format={{ style: 'percent', maximumFractionDigits: 0 }}        getAriaValueText={(formatted) => `${formatted} confidence, pleural effusion`}      >        <MeterLabel>Pleural effusion</MeterLabel>        <MeterValue />        <MeterTrack />      </Meter>    </div>  )}

qXR confidence runs 0–1, so max={1} and format turns 0.84 into 84% for display without the component pretending the underlying number was ever a percentage. getAriaValueText supplies the sentence a screen reader reads, which should name the finding — "84% confidence, consolidation" rather than a bare number arriving out of context.

A block of readings

Reporting node — Ward B
Sampled 30 seconds ago
GPU utilisation
x
Study cache
x
Concurrent reads
x
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/registry/qure/ui/card'import { Meter, MeterLabel, MeterTrack, MeterValue } from '@/registry/qure/ui/meter'/** * Capacity, which is the classic meter case: three readings taken now, none * of them going anywhere, all of them worth knowing before you accept * another study. */export default function MeterCard() {  return (    <Card className="w-full max-w-sm">      <CardHeader>        <CardTitle>Reporting node — Ward B</CardTitle>        <CardDescription>Sampled 30 seconds ago</CardDescription>      </CardHeader>      <CardContent className="grid gap-5">        <Meter tone="success" value={41}>          <MeterLabel>GPU utilisation</MeterLabel>          <MeterValue />          <MeterTrack />        </Meter>        <Meter tone="attention" value={368} max={512} format={{ style: 'unit', unit: 'gigabyte' }}>          <MeterLabel>Study cache</MeterLabel>          <MeterValue>{(formatted) => `${formatted} of 512 GB`}</MeterValue>          <MeterTrack />        </Meter>        <Meter tone="urgent" value={17} max={18}>          <MeterLabel>Concurrent reads</MeterLabel>          <MeterValue>{(_, value) => `${value} of 18`}</MeterValue>          <MeterTrack />        </Meter>      </CardContent>    </Card>  )}

Capacity is the classic case: several readings taken at the same moment, none of them going anywhere, all of them worth knowing before you accept another study. Keep the row gap generous — meters stacked tightly read as one segmented bar.

In a table

AccessionFindingConfidence
ACC-4471902Consolidation
x
0.91
ACC-4471887Nodule
x
0.55
ACC-4471863Pneumothorax
x
0.22
import { Meter, MeterTrack } from '@/registry/qure/ui/meter'/** * In a worklist there is no room for a label above the bar — the column * heading is the label. `aria-label` carries what the eye gets from the * column, and `size="sm"` keeps the row at its normal height. */const studies = [  { accession: 'ACC-4471902', finding: 'Consolidation', score: 0.91 },  { accession: 'ACC-4471887', finding: 'Nodule', score: 0.55 },  { accession: 'ACC-4471863', finding: 'Pneumothorax', score: 0.22 },]export default function MeterWorklist() {  return (    <table className="w-full max-w-lg text-sm">      <thead>        <tr className="border-b text-left text-muted-foreground">          <th className="pb-2 font-medium">Accession</th>          <th className="pb-2 font-medium">Finding</th>          <th className="pb-2 font-medium">Confidence</th>        </tr>      </thead>      <tbody>        {studies.map((s) => (          <tr key={s.accession} className="border-b last:border-0">            <td className="py-3 font-mono text-xs">{s.accession}</td>            <td className="py-3">{s.finding}</td>            <td className="w-40 py-3">              <div className="flex items-center gap-3">                <Meter                  size="sm"                  tone={s.score >= 0.75 ? 'urgent' : 'neutral'}                  value={s.score}                  max={1}                  aria-label={`${s.finding} confidence`}                >                  <MeterTrack />                </Meter>                <span className="shrink-0 tabular-nums text-muted-foreground">{s.score.toFixed(2)}</span>              </div>            </td>          </tr>        ))}      </tbody>    </table>  )}

size="sm" halves the track so a row keeps its height. There is no label because the column heading is the label, but the root still needs an aria-label: a screen reader reading across the row gets no help from a heading three rows up.

API Reference

Everything Base UI's Meter accepts, plus two of ours on the root.

Prop

Type

MeterValue takes a function as its children — (formatted, value) => ReactNode — for when the figure needs a unit or a denominator the formatter cannot supply.

On this page