charts
Registry

Usage Meter

Quota remaining with a used track and a reset caption.

Preview

Preview

Credits remaining

$500.00

of $1,000 this cycle

50% usedResets Jul 1

Payout reserve

$614.00

of $800 held

23% usedClears on payout

Loading

Loading
Loading

Installation

Install with the shadcn CLI. This copies source into your project.

bash
npx shadcn@latest add https://livedocs.xyz/r/usage-meter.json
Namespace (after one-time setup)
npx shadcn@latest add @livedocs/usage-meter

Usage

tsx
import { UsageMeter } from "@/components/ui/usage-meter"

export function Example() {
  return (
    <UsageMeter
      title="Credits remaining"
      value={500}
      max={1000}
      remainingLabel="of $1,000 this cycle"
      resetLabel="Resets Jul 1"
    />
  )
}

Props

PropTypeDefaultDescription
isLoadingbooleanShow shared loading feedback instead of quota values
valuenumberAmount used
maxnumberQuota

Source

"use client";

import * as React from "react";

import { Card } from "@/components/ui/card";
import { ChartHoverTooltip, ChartTooltipSurface } from "@/components/ui/chart";
import { ChartSkeleton, useChartReactions } from "@/components/ui/chart-reactions";
import { cn } from "@/lib/utils";

function UsageMeterRoot({
  title,
  value,
  max,
  remainingLabel,
  resetLabel,
  className,
  isLoading,
}: {
  title: string;
  value: number;
  max: number;
  remainingLabel?: string;
  resetLabel?: string;
  className?: string;
  isLoading?: boolean;
}) {
  const settings = useChartReactions();
  isLoading = isLoading || Boolean(settings.isLoading);
  const used = max <= 0 ? 0 : Math.max(0, Math.min(1, value / max)) * 100;
  const left = Math.max(0, max - value);
  const [active, setActive] = React.useState<"used" | "remaining">();
  const formatAmount = (amount: number) => amount.toLocaleString("en-US", { style: "currency", currency: "USD" });
  const tooltip = <ChartTooltipSurface title={title}>
    <div className="flex items-center justify-between gap-6"><span className="text-muted-foreground">Used</span><span className="font-mono font-bold tabular-nums">{formatAmount(value)}</span></div>
    <div className="flex items-center justify-between gap-6"><span className="text-muted-foreground">Remaining</span><span className="font-mono font-bold tabular-nums">{formatAmount(left)}</span></div>
    <div className="flex items-center justify-between gap-6"><span className="text-muted-foreground">Limit</span><span className="font-mono font-bold tabular-nums">{formatAmount(max)}</span></div>
  </ChartTooltipSurface>;

  return (
    <Card className={cn("p-5", className)}>
      <ChartSkeleton isLoading={isLoading}>
        <p className="text-[15px] font-medium tracking-tight">{title}</p>
        <p className="mt-1 tabular-nums text-4xl font-medium tracking-tight">
          {left.toLocaleString("en-US", {
            style: "currency",
            currency: "USD",
          })}
        </p>
        {remainingLabel ? (
          <p className="mt-1 text-xs text-muted-foreground">{remainingLabel}</p>
        ) : null}
        <div className="mt-4 flex h-3 rounded-full bg-muted" role="group" aria-label={`${title} usage`}>
          {(["used", "remaining"] as const).map((part) => (
            <div key={part} className="h-full" style={{ width: `${part === "used" ? used : 100 - used}%` }}>
              <ChartHoverTooltip className="h-full" content={tooltip} onActiveChange={(next: boolean) => setActive((current) => next ? part : current === part ? undefined : current)}>
                <div
                  tabIndex={(part === "used" ? used : 100 - used) > 0 ? 0 : -1}
                  role="img"
                  aria-label={`${part === "used" ? "Used" : "Remaining"}: ${formatAmount(part === "used" ? value : left)} of ${formatAmount(max)}`}
                  className={cn("h-full transition-opacity motion-reduce:transition-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", part === "used" ? "rounded-full bg-primary" : "rounded-r-full bg-muted", active && active !== part && "opacity-60")}
                />
              </ChartHoverTooltip>
            </div>
          ))}
        </div>
        <div className="mt-2 flex justify-between text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
          <span>{used.toFixed(0)}% used</span>
          {resetLabel ? <span>{resetLabel}</span> : null}
        </div>
      </ChartSkeleton>
    </Card>
  );
}

export const UsageMeter = Object.assign(UsageMeterRoot, {});