Lunia DS

Feedback and live

Usage meter

A bar or small ring that shows how much of a limit is left, where the numbers roll as they are spent, the fill turns amber when little is left and red when nothing is left, and an upgrade link opens only when it matters.

Loading preview

A "Monthly credits" bar with a limit of 2,000 that resets on Oct 12, plus the ring variant beside a message box. The demo spends credits in steps (760, then 1,640, then 2,000) so the meter passes Low and reaches Out, and the upgrade link opens; Replay sets used back to 760 and runs the steps again.

Installation

Run this in your app after the one-time setup on the Getting started page.

$ npx @ganisigit/lunia-ds add usage-meter

Usage

The demo above, as code.

demo.tsx
'use client';

import * as React from 'react';
import { UsageMeter } from './usage-meter';

const LIMIT = 2000;
const RESET = new Date(2026, 9, 12);
// Credits spent in steps, so the meter passes Low and reaches Out.
const STEPS: [at: number, used: number][] = [
  [1400, 1640],
  [2900, 2000],
];

export default function UsageMeterDemo({ replay = 0 }: { replay?: number }) {
  const [used, setUsed] = React.useState(760);

  // Replay sets the credits back to 760 and spends them again.
  React.useEffect(() => {
    if (replay === 0) return;
    setUsed(760);
    const timers = STEPS.map(([at, value]) => window.setTimeout(() => setUsed(value), at));
    return () => {
      for (const t of timers) window.clearTimeout(t);
    };
  }, [replay]);

  return (
    <div className="flex w-full max-w-[400px] flex-col gap-8 rounded-xl border border-secondary bg-primary p-6">
      <UsageMeter
        used={used}
        limit={LIMIT}
        label="Monthly credits"
        unit="credits"
        resetAt={RESET}
        onUpgrade={() => {}}
      />
      <div className="flex flex-col gap-3 rounded-2xl border border-secondary bg-primary_alt p-4 shadow-xs">
        <p className="text-sm text-tertiary">Write a message</p>
        <div className="flex items-center justify-end gap-3">
          <UsageMeter
            variant="ring"
            used={used}
            limit={LIMIT}
            label="Monthly credits"
            unit="credits"
            resetAt={RESET}
          />
        </div>
      </div>
    </div>
  );
}

API

Parts: root, label, count (number roll: left, of, limit), unit line (text morph), track, fill, reset line (text morph), upgrade link (opens when low or out), ring (ring variant: track circle, fill arc, percent).

PropTypeDefaultDescription
usednumber-Amount already used. Required.
limitnumber-Total amount for the period. Required.
unitstring"credits"Plural name of what is counted, for example "credits" or "messages".
labelstring"Usage"Name of the meter, shown above the bar and used as the accessible name.
resetAtnumber | DateundefinedWhen the amount resets (ms or Date); shows the reset line.
variant"bar" | "ring""bar"A full-width bar for settings and menus, or a 16px ring with a percent for toolbars.
lowAtnumber0.2Share left (0 to 1) at which the meter turns amber and shows the upgrade link.
upgradeLabelstring"Upgrade"Text of the upgrade link.
EventDescription
upgradeFires when the upgrade link is pressed; the link only shows when this event has a listener.

Motion

Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.

IDTriggerElementPropertyFrom → toTimingReduced motion
UM1used changesfill (bar)widthold share → new shareduration-4xl (520)ease-settleResizes at once
UM2level changes (Normal, Low, Out)fill (bar)background-colorfg-primary → fg-warning-primary (Low) or fg-error-primary (Out)duration-4xl (520)ease-settleSwaps at once
UM3used or limit changescountnumber rollold number → new numberduration-5xl (640)ease-outSwaps at once
UM4level changes to or from Outcount + unit linecolortext-tertiary → text-error-primaryduration-lg (320)ease-outSwaps at once
UM5level changes to Outcountgrid-template-columns (fold away) + opacity + filter blur1fr, 1, 0 → 0fr, 0, blur-swapduration-3xl (460)ease-outHides at once
UM6level changes from Out (limit raised or reset)countgrid-template-columns (fold open) + opacity + filter blur0fr, 0, blur-swap → 1fr, 1, 0duration-3xl (460)ease-outShows at once
UM7level changes to or from Outunit linetext morph"credits left" → "No credits left"duration-3xl (460)ease-outSwaps at once
UM8level changes to or from Outreset linetext morph"Resets Oct 12" → "You can use more on Oct 12."duration-3xl (460)ease-outSwaps at once
UM9level changes to Low or Outupgrade linkgrid-template-columns (opens beside the reset line) + opacity + filter blur0fr, 0, blur-swap → 1fr, 1, 0duration-3xl (460)ease-outShows at once
UM10level changes back to Normalupgrade linkgrid-template-columns (fold away) + opacity + filter blur1fr, 1, 0 → 0fr, 0, blur-swapduration-3xl (460)ease-outHides at once
UM11used changes (ring variant)fill arcstroke-dashoffsetold share → new shareduration-4xl (520)ease-outResizes at once
UM12level changes (ring variant)fill arcstroke colorfg-primary → fg-warning-primary or fg-error-primaryduration-4xl (520)ease-outSwaps at once
UM13used changes (ring variant)percentnumber rollold percent → new percentduration-5xl (640)ease-outSwaps at once

Accessibility

  • The bar track (or the ring) has role meter with aria-label from label, aria-valuemin 0, aria-valuemax = limit, aria-valuenow = used, and aria-valuetext with the plain summary, for example "1,240 of 2,000 credits left" or "No credits left".
  • The visible count and unit line are aria-hidden, because they repeat the meter's value text and the rolling digits would be read one by one.
  • The ring variant shows the summary and the reset date in a tooltip-style title, because it has no room for text.
  • The upgrade link is inert and aria-hidden while it is folded away, so it is not in the tab order at the Normal level.
  • Color is never the only signal: at the Out level the words change to "No credits left" and the reset line says when more is available.
  • Reduced motion: the fill, numbers and folds change at once and the text swaps at once.