Feedback

Meter

Accessible meter built on react-aria-components, styled with the quebi design system. Shows a value within a known range with a brand-teal fill that shifts to amber and red past warning and danger thresholds.

meterprogressgaugefeedbackusage

Default

A labelled meter with a header, formatted value, and track.

Storage used42%

Thresholds

The fill is brand teal below 70%, amber from 70-90%, and red at 90% and above.

Normal45%
Warning78%
Danger95%

Formatted value

Use formatOptions to render the value as a currency or unit.

Budget spent$620.00

Custom color

Pin an explicit fill color to bypass the threshold logic.

Score88%

Live

Live usage30%

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { createContext, use } from "react"
import {
  Meter as MeterPrimitive,
  type MeterProps as MeterPrimitiveProps,
  type MeterRenderProps as MeterPrimitiveRenderProps,
} from "react-aria-components"
import { cn } from "@/lib/utils"

/**
 * Meter — quebi design system
 *
 * Built on react-aria-components. A labelled progress-style bar for a known
 * range (storage used, quota, score). The fill defaults to brand teal and
 * shifts to amber past a warning threshold and red past a danger threshold,
 * or you can pin an explicit color. Composed from Meter, MeterHeader,
 * MeterValue, and MeterTrack.
 */

interface MeterRenderProps extends MeterPrimitiveRenderProps {
  color?: string
}

const MeterContext = createContext<MeterRenderProps | null>(null)

interface MeterProps extends MeterPrimitiveProps, Pick<MeterRenderProps, "color"> {}

export function Meter({ className, children, color, ...props }: MeterProps) {
  return (
    <MeterPrimitive
      data-slot="meter"
      {...props}
      className={cn(
        "w-full",
        "[&>[data-slot=meter-header]+[data-slot=meter-track]]:mt-2",
        "[&>[data-slot=meter-header]+[slot='description']]:mt-1",
        "[&>[slot='description']+[data-slot=meter-track]]:mt-2",
        "[&>[data-slot=meter-track]+[slot=description]]:mt-2",
        "[&>[data-slot=meter-track]+[slot=errorMessage]]:mt-2",
        "*:data-[slot=meter-header]:font-medium",
        className,
      )}
    >
      {(values) => (
        <MeterContext value={{ ...values, color }}>
          {typeof children === "function" ? children(values) : children}
        </MeterContext>
      )}
    </MeterPrimitive>
  )
}

export function MeterHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="meter-header"
      className={cn("flex items-center justify-between text-sm text-white", className)}
      {...props}
    />
  )
}

export function MeterValue({
  className,
  ...props
}: Omit<React.ComponentProps<"span">, "children">) {
  const ctx = use(MeterContext)
  if (!ctx) throw new Error("MeterValue must be used within a Meter")
  const { valueText } = ctx
  return (
    <span
      data-slot="meter-value"
      className={cn("text-sm text-quebi-fg-muted tabular-nums", className)}
      {...props}
    >
      {valueText}
    </span>
  )
}

export function MeterTrack({ className, ...props }: React.ComponentProps<"div">) {
  const ctx = use(MeterContext)
  if (!ctx) throw new Error("MeterTrack must be used within a Meter")
  const { percentage, color } = ctx
  return (
    <div
      data-slot="meter-track"
      className={cn(
        "relative h-1.5 w-full overflow-hidden rounded-full border border-cyan-500/10 bg-cyan-500/10",
        className,
      )}
      {...props}
    >
      <div
        data-slot="meter-fill"
        className="absolute start-0 top-0 h-full rounded-full transition-[width] duration-200 ease-linear will-change-[width] motion-reduce:transition-none forced-colors:bg-[Highlight]"
        style={{ width: `${percentage}%`, backgroundColor: color ?? getMeterColor(percentage) }}
      />
    </div>
  )
}

function getMeterColor(value: number): string {
  if (value < 70) return "var(--color-quebi-brand)"
  if (value < 90) return "var(--color-amber-500)"
  return "var(--color-red-500)"
}