Meter
Static measurement bar (role=meter) for a known range, with optional segments. Install Meter from the VLLNT UI registry with the shadcn CLI.
Static measurement bar (role=meter) for a known range, with optional segments. Part of the core family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Meter to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/meter.jsonSource
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 };
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.