import { Meter } from "@/components/ui/meter" ;
export default function MeterDemo () {
return (
< div className = "flex w-full max-w-xs flex-col gap-5" >
< Meter label = "Storage" value = { 42 } />
< Meter label = "Memory" value = { 81 } />
< Meter label = "CPU" value = { 95 } />
</ div >
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/meter
The CLI installs dependencies and any other components this one uses.
import { Meter } from "@/components/ui/meter" ;
< Meter label = "Storage" value = { 72 } />
Without a color, the bar is success below 75%, warning from 75% and danger from 90%. Change the cut-offs with thresholds, or set color to use one fixed tone.
Meter — a level within a known range that can go up or down: disk usage, seats, budget spent, signal or password strength.
Progress Bar — the completion of a task that is running and will finish.
Spinner and Skeleton — waiting for something to load, where there's no value to show.
< Meter label = "…" > { /* role="meter progressbar" */ }
{ /* header: Label + value text */ }
{ /* track + fill, or a row of segments */ }
</ Meter >
Part Renders Notes Meter<div role="meter progressbar">React Aria Meter. Sets aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext. Label Label (<span>)Rendered when label is set and used as the accessible name. Value text <span>The formatted value, or valueLabel. Hidden with showValue={false}. Track + fill <div> › <div>Muted track with a fill whose width follows the percentage (500ms transition). Segments <div> × segmentsReplaces the track when segments is set. A segment fills once the value reaches its share of the range.
thresholds takes two percentages: where the bar turns warning and where it turns danger. They're measured against the percentage of the range, not the raw value.
import { Meter } from "@/components/ui/meter" ;
export default function MeterThresholds () {
return (
< div className = "flex w-full max-w-xs flex-col gap-5" >
< Meter label = "Error budget used" value = { 35 } thresholds = {[ 50 , 80 ]} />
< Meter label = "Error budget used" value = { 62 } thresholds = {[ 50 , 80 ]} />
< Meter label = "Error budget used" value = { 86 } thresholds = {[ 50 , 80 ]} />
</ div >
);
}
Set color when the value isn't "good" or "bad", such as a score or a completeness level. color always wins over thresholds.
import { Meter } from "@/components/ui/meter" ;
export default function MeterColors () {
return (
< div className = "flex w-full max-w-xs flex-col gap-5" >
< Meter label = "Profile completeness" value = { 70 } color = "brand" />
< Meter label = "Match score" value = { 88 } color = "info" />
< Meter label = "Seats used" value = { 96 } color = "primary" />
< Meter label = "Disk usage" value = { 40 } color = "neutral" />
</ div >
);
}
Thresholds assume higher is worse. For values like battery or health scores, compute the color yourself and pass it in.
import { Meter } from "@/components/ui/meter" ;
function batteryColor ( level : number ) {
if (level <= 15 ) return "danger" ;
if (level <= 30 ) return "warning" ;
return "success" ;
}
export default function MeterLowIsBad () {
return (
< div className = "flex w-full max-w-xs flex-col gap-5" >
{[ 12 , 28 , 84 ]. map (( level ) => (
< Meter
key = {level}
label = "Battery"
value = {level}
segments = { 10 }
color = { batteryColor (level)}
/>
))}
</ div >
);
}
md (8px) is the default; sm (6px) suits cards, tables and dense dashboards.
import { Meter } from "@/components/ui/meter" ;
export default function MeterSizes () {
return (
< div className = "flex w-full max-w-xs flex-col gap-5" >
< Meter label = "Small" size = "sm" value = { 48 } />
< Meter label = "Medium" size = "md" value = { 48 } />
</ div >
);
}
segments splits the bar into equal pieces, for battery, signal or rating-style displays. Segments fill in whole steps, so pick a count that divides your range cleanly.
import { Meter } from "@/components/ui/meter" ;
export default function MeterSegments () {
return (
< div className = "flex w-full max-w-xs flex-col gap-5" >
< Meter label = "Battery" value = { 70 } segments = { 10 } color = "success" />
< Meter
label = "Signal"
value = { 2 }
maxValue = { 4 }
segments = { 4 }
valueLabel = "Fair"
color = "warning"
/>
< Meter label = "Rack capacity" value = { 90 } segments = { 20 } size = "sm" />
</ div >
);
}
valueLabel replaces the percentage with your own text. It's also used as aria-valuetext, so pass a string that reads well on its own.
API requests 48.2k / 50k this month
import { Meter } from "@/components/ui/meter" ;
export default function MeterValueLabel () {
return (
< div className = "flex w-full max-w-xs flex-col gap-5" >
< Meter
label = "Storage"
value = { 7.2 }
maxValue = { 10 }
valueLabel = "7.2 of 10 GB"
/>
< Meter
label = "API requests"
value = { 48200 }
maxValue = { 50000 }
valueLabel = "48.2k / 50k this month"
/>
</ div >
);
}
Set minValue and maxValue to work in real units, and formatOptions to format the value in the user's locale. With the default percent style the percentage is shown; any other style formats the raw value.
import { Meter } from "@/components/ui/meter" ;
export default function MeterFormat () {
return (
< div className = "flex w-full max-w-xs flex-col gap-5" >
< Meter
label = "Monthly budget"
value = { 3240 }
maxValue = { 5000 }
formatOptions = {{
style: "currency" ,
currency: "USD" ,
maximumFractionDigits: 0 ,
}}
/>
< Meter
label = "Bandwidth"
value = { 412 }
maxValue = { 500 }
formatOptions = {{ style: "unit" , unit: "gigabyte" }}
/>
< Meter
label = "Temperature"
value = { 68 }
minValue = { 20 }
maxValue = { 90 }
formatOptions = {{ style: "unit" , unit: "celsius" }}
/>
</ div >
);
}
In tables and lists, pass aria-label and hide the value with showValue={false} if you show it elsewhere.
import { Meter } from "@/components/ui/meter" ;
const regions = [
{ name: "us-east-1" , load: 64 },
{ name: "eu-west-1" , load: 82 },
{ name: "ap-south-1" , load: 93 },
];
export default function MeterNoLabel () {
return (
< div className = "w-full max-w-sm divide-y rounded-xl border bg-card text-sm" >
{regions. map (( r ) => (
< div key = {r.name} className = "flex items-center gap-4 px-4 py-2.5" >
< span className = "w-24 shrink-0 font-mono text-xs" >{r.name}</ span >
< Meter
aria-label = { `${ r . name } load` }
value = {r.load}
showValue = { false }
size = "sm"
/>
< span className = "w-10 shrink-0 text-right text-muted-foreground tabular-nums" >
{r.load}%
</ span >
</ div >
))}
</ div >
);
}
Plan limits in a billing card. Each meter uses maxValue and a valueLabel in real units, and turns amber and red as it approaches the limit.
Build minutes 2,860 of 3,000 min
import { Button } from "@/components/ui/button" ;
import { Meter } from "@/components/ui/meter" ;
const quotas = [
{ label: "Seats" , value: 8 , max: 10 , unit: "members" },
{ label: "Storage" , value: 41.6 , max: 50 , unit: "GB" },
{ label: "Build minutes" , value: 2860 , max: 3000 , unit: "min" },
{ label: "Projects" , value: 4 , max: 20 , unit: "projects" },
];
export default function MeterRecipeUsageQuotas () {
return (
< div className = "w-full max-w-sm rounded-xl border bg-card" >
< div className = "flex items-start justify-between gap-4 border-b p-4" >
< div >
< h3 className = "font-semibold" >Pro plan</ h3 >
< p className = "text-muted-foreground text-sm" >Resets on July 1</ p >
</ div >
< Button size = "sm" variant = "outline" >
Upgrade
</ Button >
</ div >
< div className = "grid gap-5 p-4" >
{quotas. map (( q ) => (
< Meter
key = {q.label}
label = {q.label}
value = {q.value}
maxValue = {q.max}
valueLabel = { `${ q . value . toLocaleString () } of ${ q . max . toLocaleString () } ${ q . unit }` }
size = "sm"
/>
))}
</ div >
</ div >
);
}
A segmented meter under a TextField , with a color and label computed from a simple score.
import { useState } from "react" ;
import { Meter } from "@/components/ui/meter" ;
import { TextField } from "@/components/ui/text-field" ;
const levels = [
{ label: "Too short" , color: "danger" },
{ label: "Weak" , color: "danger" },
{ label: "Fair" , color: "warning" },
{ label: "Good" , color: "success" },
{ label: "Strong" , color: "success" },
] as const ;
function score ( password : string ) {
if (password. length < 8 ) return 0 ;
let s = 1 ;
if ( / [A-Z] / . test (password) && / [a-z] / . test (password)) s ++ ;
if ( / \d / . test (password)) s ++ ;
if ( / [ ^ A-Za-z0-9] / . test (password) || password. length >= 14 ) s ++ ;
return s;
}
export default function MeterRecipePasswordStrength () {
const [ password , setPassword ] = useState ( "" );
const s = score (password);
const level = levels[s];
return (
< div className = "flex w-full max-w-xs flex-col gap-3" >
< TextField
label = "New password"
type = "password"
value = {password}
onChange = {setPassword}
description = "At least 8 characters. Mix cases, numbers and symbols."
/>
< Meter
label = "Strength"
value = {password ? s : 0 }
maxValue = { 4 }
segments = { 4 }
size = "sm"
valueLabel = {password ? level.label : "—" }
color = {level.color}
/>
</ div >
);
}
Compact meters in cards, with a Badge summarizing the worst reading.
import { Badge } from "@/components/ui/badge" ;
import { Meter } from "@/components/ui/meter" ;
const servers = [
{ name: "web-01" , region: "fra1" , cpu: 38 , memory: 61 , disk: 44 },
{ name: "web-02" , region: "fra1" , cpu: 77 , memory: 72 , disk: 45 },
{ name: "db-primary" , region: "ams3" , cpu: 54 , memory: 91 , disk: 83 },
];
function status ( s : ( typeof servers)[ number ]) {
const max = Math. max (s.cpu, s.memory, s.disk);
if (max >= 90 ) return { label: "Critical" , color: "danger" } as const ;
if (max >= 75 ) return { label: "Degraded" , color: "warning" } as const ;
return { label: "Healthy" , color: "success" } as const ;
}
export default function MeterRecipeServerHealth () {
return (
< div className = "grid w-full max-w-2xl gap-3 sm:grid-cols-3" >
{servers. map (( s ) => {
const st = status (s);
return (
< div key = {s.name} className = "rounded-xl border bg-card p-4" >
< div className = "mb-4 flex items-start justify-between gap-2" >
< div >
< p className = "font-medium font-mono text-sm" >{s.name}</ p >
< p className = "text-muted-foreground text-xs" >{s.region}</ p >
</ div >
< Badge variant = "dot" color = {st.color} size = "sm" >
{st.label}
</ Badge >
</ div >
< div className = "grid gap-3" >
< Meter label = "CPU" value = {s.cpu} size = "sm" />
< Meter label = "Memory" value = {s.memory} size = "sm" />
< Meter label = "Disk" value = {s.disk} size = "sm" />
</ div >
</ div >
);
})}
</ div >
);
}
Renders role="meter progressbar": browsers that support the meter role use it, others fall back to progressbar.
aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext are set for you. aria-valuetext is your valueLabel, or the value formatted with formatOptions.
Every meter needs an accessible name: label, aria-label or aria-labelledby.
Color reinforces the value but never replaces it; keep the value text visible or describe the state in valueLabel ("Fair", "Critical").
Meters aren't live regions. Changes aren't announced until the user reaches the meter.
Slot Element data-slot="meter"Root data-slot="label"The visible label
className accepts a function of the render state (percentage, valueText):
< Meter
label = "Disk"
value = {value}
className = {({ percentage }) => (percentage >= 90 ? "font-medium" : "" )}
/>
The resolved tone sets --tone on the track (or the segment row), and the fill uses bg-(--tone). The track itself is bg-muted.
Also accepts every prop of React Aria's Meter , such as id, style and aria-describedby.
Progress Bar — the progress of a running task.
Alert — pair a meter with a warning when a limit is close.
Badge — a compact status next to a reading.