Elements

Elements · Observability

Quota banner

How much is left, when it comes back, and the way to get more.

18 messages leftresets in 3h 12m
32 of 50 used
fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-quota-banner"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

QuotaBanner shows what's left of a usage allowance: a headline count, a fill bar, and an upgrade button that turns amber once you're close to the limit. assistant-ui's runtime has no concept of plan limits or rate budgets, that's account and billing state the host app owns, so this element has just the one, standalone form: you hold the used and limit numbers and re-render as they change.

Getting started

You decide what "used" and "limit" mean, in whatever unit your plan counts, messages, tokens, or requests, and pass the current numbers down from wherever your account state lives.

Hold the usage state

app/quota.tsx
"use client";

import { useState } from "react";
import { QuotaBanner } from "@/components/assistant-ui/elements/quota-banner";

export function Quota() {
  const [used, setUsed] = useState(32);

  return (
    <QuotaBanner
      used={used}
      limit={50}
      unit="messages"
      resetsIn="3h 12m"
      upgradeLabel="Upgrade"
      onUpgrade={() => (window.location.href = "/billing")}
    />
  );
}

Recompute resetsIn on your own clock

The component never counts down on its own. resetsIn is whatever string you pass, so refresh it yourself, from a timer or from the account's reset timestamp:

const resetsIn = formatDistanceToNowStrict(resetAt);

Anatomy

<div data-slot="quota-banner">
  <div>
    <span>{/* "{left} {unit} left", amber past 90% used */}</span>
    <span>{/* "resets in {resetsIn}" */}</span>
  </div>
  <span role="meter">{/* bar, named from the unit, amber past 90% used */}</span>
  <div>
    <span>{/* "{used} of {limit} used" */}</span>
    <button>{/* upgradeLabel, always rendered */}</button>
  </div>
</div>

left is limit minus used, floored at zero, so going over quota reads as "0 {unit} left" rather than a negative number. The tight, amber state starts at 90% used (used / limit >= 0.9), coloring both the headline and the fill bar; a limit of 0 is treated as 0% used rather than producing NaN. The bar's track is a named meter whose 0…100 value matches the painted used share and whose value text reads the same used of limit the footer prints; the value sits on the track rather than the fill, which collapses to nothing at zero. The upgrade button always renders, whether or not the account is close to its limit, and calls onUpgrade if you pass one, there's no default action if you don't.

Examples

Restyle the banner

The fill bar's filled portion and the button both read the shared surface tokens from surfaces.tsx, the button is inkButton, and the root is paper. className on the root merges with these.

<QuotaBanner className="max-w-none" /* ... */ />

Reading a plan's usage from your billing API

const { data } = useSWR("/api/usage", fetcher);
if (!data) return null;

return (
  <QuotaBanner
    used={data.used}
    limit={data.limit}
    unit={data.unit}
    resetsIn={data.resetsIn}
    upgradeLabel="Upgrade"
    onUpgrade={() => router.push("/billing")}
  />
);

API reference

QuotaBanner

PropTypeDefaultDescription
usednumberrequiredUnits consumed so far.
limitnumberrequiredUnits allowed before reset.
unitstringrequiredLabel for the count, for example "messages".
resetsInstringrequiredPre-formatted countdown text.
upgradeLabelstringrequiredText on the upgrade button.
onUpgrade() => voidCalled when the upgrade button is pressed.
classNamestringMerged onto the root.

All other div props are forwarded to the root.