Meter

Static measurement bar (role=meter) for a known range, with optional segments.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/meter.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

3 stories available:

Code

import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "../../lib/utils"; const meterFillVariants = cva("h-full transition-all", { defaultVariants: { variant: "default", }, variants: { variant: { default: "bg-primary", destructive: "bg-destructive", secondary: "bg-secondary-foreground", }, }, }); function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } /** Props for the {@link Meter} component. */ export type MeterProps = { /** Accessible label naming the measured quantity. */ label?: string; /** Upper bound of the measured range. Defaults to `100`. */ max?: number; /** Lower bound of the measured range. Defaults to `0`. */ min?: number; /** Split the bar into this number of discrete blocks instead of a solid fill. */ segments?: number; /** Current measured value (clamped to `min`/`max`). */ value: number; /** Human-readable description of the current value (`aria-valuetext`). */ valueText?: string; } & Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & VariantProps<typeof meterFillVariants>; /** * Static measurement bar for a known range (disk usage, score, capacity). * Uses `role="meter"` — distinct from a progress bar, which reports task * completion over time. * @example * <Meter label="Disk usage" value={72} valueText="72% used" /> */ const Meter = ({ className, label, max = 100, min = 0, ref, segments, value, valueText, variant, ...props }: MeterProps & { ref?: React.Ref<HTMLDivElement> }) => { const safeMax = max > min ? max : min + 1; const current = clamp(value, min, safeMax); const ratio = (current - min) / (safeMax - min); const percentage = ratio * 100; const segmentCount = segments !== undefined && segments > 0 ? segments : 0; const filledSegments = Math.round(ratio * segmentCount); return ( <div aria-label={label} aria-valuemax={safeMax} aria-valuemin={min} aria-valuenow={current} aria-valuetext={valueText} className={cn( "h-2 w-full overflow-hidden rounded-full bg-muted", segmentCount > 0 && "flex gap-0.5 bg-transparent", className, )} ref={ref} role="meter" {...props} > {segmentCount > 0 ? ( Array.from({ length: segmentCount }, (_cell, index) => ( <div className={cn( "h-full flex-1 rounded-full", index < filledSegments ? meterFillVariants({ variant }) : "bg-muted", )} key={`meter-segment-${index}`} /> )) ) : ( <div className={meterFillVariants({ variant })} style={{ width: `${percentage}%` }} /> )} </div> ); }; Meter.displayName = "Meter"; export { Meter, meterFillVariants };

Dependencies

  • @vllnt/ui@^0.2.1