Elements · Observability
Quota banner
How much is left, when it comes back, and the way to get more.
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 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, 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
"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
| 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.