For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Meter

The Meter component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/meter

Source

"use client";

import { Meter as MeterPrimitive } from "@base-ui/react/meter";
import { cn } from "cn";
import type { ReactNode } from "react";

import type { ProgressSize } from "@/components/ui/progress";

// The fill moves exactly like Progress: the value changes on the compositor,
// a 500ms translate with the soft landing curve, so the rail never repaints.
const EASE = "ease-[cubic-bezier(0.22,1,0.36,1)]";

// Track thickness in px: 4, 6 (default), 8.
const trackHeights: Record<ProgressSize, string> = {
  default: "h-1.5",
  lg: "h-2",
  sm: "h-1",
};

export type MeterVariant = "default" | "success" | "warning" | "destructive";

// The rail draws its fill from the text colour, so a variant is just a text
// class. Default, success and destructive reuse the Progress and notification
// tokens, which already hold the source's values in both modes.
const variants: Record<MeterVariant, string> = {
  default: "text-ring",
  destructive:
    "text-[color:var(--notification-error-foreground,var(--destructive))]",
  success: "text-[color:var(--notification-success-foreground,var(--chart-2))]",
  warning: "text-[color:var(--status-dot-yellow-dot,var(--primary))]",
};

function clampPercentage(value: number, min: number, max: number) {
  const range = max - min;
  const percentage = range === 0 ? 0 : ((value - min) / range) * 100;
  if (!Number.isFinite(percentage)) {
    return 0;
  }
  return Math.min(100, Math.max(0, percentage));
}

type MeterProps = Omit<MeterPrimitive.Root.Props, "children" | "className"> & {
  className?: string;
  /** Helper text under the bar. */
  description?: ReactNode;
  /** Label above the bar, also the accessible name. */
  label?: ReactNode;
  /** Show the formatted value at the end of the label row. Default: shown with a label. */
  showValueLabel?: boolean;
  /** Track thickness: 4, 6 (default), or 8px. */
  size?: ProgressSize;
  /** Colour of the fill. Default `default`. */
  variant?: MeterVariant;
};

/**
 * Meter — a quantity within a known range: storage used, seats taken, a
 * password's strength. Unlike Progress it describes a level rather than a
 * task moving towards done, which Base UI announces through `role="meter"`.
 *
 *   <Meter label="Storage" value={72} />
 *   <Meter label="Seats" value={19} max={20} variant="warning" />
 *
 * It is drawn on Progress' track: the same rail, fill and 500ms eased
 * translate when the value changes. `variant` colours the fill from semantic
 * tokens, so a meter can turn amber or red as it fills up.
 */
function Meter({
  className,
  description,
  label,
  max = 100,
  min = 0,
  showValueLabel,
  size = "default",
  variant = "default",
  value,
  ...props
}: MeterProps) {
  const showValue = showValueLabel ?? label != null;
  const percentage = clampPercentage(value, min, max);

  return (
    <MeterPrimitive.Root
      data-slot="meter"
      min={min}
      max={max}
      value={value}
      className={cn("flex w-full flex-col gap-1.5", className)}
      {...props}
    >
      {label != null || showValue ? (
        <div className="flex items-baseline justify-between gap-3">
          {label != null ? (
            <MeterPrimitive.Label
              data-slot="meter-label"
              className="flex min-w-0 items-center gap-0.5 truncate text-sm font-medium text-foreground"
            >
              {label}
            </MeterPrimitive.Label>
          ) : (
            <span />
          )}
          {showValue ? (
            <MeterPrimitive.Value
              data-slot="meter-value"
              className="shrink-0 text-sm font-medium tabular-nums text-[color:var(--foreground-tertiary,var(--muted-foreground))]"
            />
          ) : null}
        </div>
      ) : null}
      <MeterPrimitive.Track
        data-slot="meter-track"
        className={cn(
          // The rail keeps an edge on every surface in both themes, and the
          // fill draws from the text colour the variant sets.
          "relative w-full rounded-full bg-border",
          trackHeights[size],
          variants[variant]
        )}
      >
        <MeterPrimitive.Indicator
          data-slot="meter-indicator"
          // Base UI sizes the indicator by width; the full width here lets the
          // inner fill move by translate instead.
          style={{ width: "100%" }}
          className="absolute inset-0 overflow-hidden rounded-full"
        >
          <span
            className={cn(
              "absolute inset-0 rounded-full bg-current transition-[translate] duration-500 rtl:-scale-x-100 motion-reduce:transition-none",
              EASE
            )}
            style={{ translate: `${percentage - 100}% 0` }}
          />
        </MeterPrimitive.Indicator>
      </MeterPrimitive.Track>
      {description ? (
        <span className="text-xs leading-4 font-medium tracking-[0.15px] text-[color:var(--foreground-tertiary,var(--muted-foreground))]">
          {description}
        </span>
      ) : null}
    </MeterPrimitive.Root>
  );
}

export { Meter };
export type { MeterProps };