Quota banner
How much is left, when it comes back, and the way to get more.
Installation
Install manually
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 initThen 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.
npx shadcn@latest add "@assistant-ui/elements-quota-banner"Props-driven: no runtime or provider required.
QuotaBanner shows what's left of a usage allowance: a headline count and a fill bar that turn amber once you're close to the limit and red once it's spent, plus an upgrade button. 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
"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, red at 0 left */}</span>
<span>{/* "resets in {resetsIn}" */}</span>
</div>
<span role="meter">{/* bar, named from the unit, amber past 90% used, red at 0 left */}</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; once nothing is left (left is 0 with a limit above 0) the exhausted state replaces it and colors both red. A limit of 0 is treated as 0% used rather than producing NaN, and never reads as exhausted. 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 button and the root read 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
| Prop | Type | Default | Description |
|---|---|---|---|
used | number | required | Units consumed so far. |
limit | number | required | Units allowed before reset. |
unit | string | required | Label for the count, for example "messages". |
resetsIn | string | required | Pre-formatted countdown text. |
upgradeLabel | string | required | Text on the upgrade button. |
onUpgrade | () => void | Called when the upgrade button is pressed. | |
className | string | Merged onto the root. |
All other div props are forwarded to the root.