Agent Limits
Animated grouped agent usage limits and token budgets.
Limits
Grouped usage limits and token budgets.
Plan usage limits · Max (5x)
5-hour limitResets in 2 hr 46 min38%
Weekly · all modelsResets Tue 3:00 PM3%
Weekly · frontier modelsResets Tue 3:00 PM5%
function AgentLimitsDemo() {
return <AgentLimitsCard context={AGENT_LIMITS_CONTEXT} limits={AGENT_PLAN_LIMITS} />;
}function AgentLimitsDemo() {
return <AgentLimitsCard context={AGENT_LIMITS_CONTEXT} limits={AGENT_PLAN_LIMITS} />;
}Installation
npx shadcn@latest add https://boardcn.dev/r/agent-limits-card.jsonnpx shadcn@latest add https://boardcn.dev/r/agent-limits-card.jsonnpm packages
- @remixicon/react
- motion
BoardCN dependencies
The CLI installs these for you — you do not need to add them yourself.
Source
The file the CLI copies into your project.
"use client";
import { useState } from "react";
import {
RiArrowDownSLine,
RiArrowRightLine,
RiArrowRightSLine,
} from "@remixicon/react";
import { AnimatePresence, motion } from "motion/react";
import { cx } from "@/utils/cx";
export type AgentLimitSegment = {
label: string;
tokens: number;
/** Optional CSS color. Falls back to the BoardCN chart palette. */
color?: string;
/** Deferred segments appear in the breakdown but do not count toward usage. */
deferred?: boolean;
};
export type AgentLimitGroupItem = {
label: string;
tokens: number;
};
export type AgentLimitGroup = {
label: string;
tokens: number;
items: readonly AgentLimitGroupItem[];
};
export type AgentContextLimit = {
max: number;
segments: readonly AgentLimitSegment[];
groups?: readonly AgentLimitGroup[];
};
export type AgentPlanLimit = {
label: string;
/** Used capacity expressed as a ratio from 0 to 1. */
used: number;
resets: string;
};
export type AgentLimitsCardProps = {
context: AgentContextLimit;
plan?: string;
planHref?: string;
limits: readonly AgentPlanLimit[];
defaultExpanded?: boolean;
onExpandedChange?: (open: boolean) => void;
className?: string;
};
const CHART_PALETTE = [2, 6, 5, 3, 8, 7, 4, 1].map((tone) => ({
color: `var(--color-chart-${tone})`,
activeColor: `var(--color-chart-${tone}-active)`,
}));
const EXPAND_EASING = [0.22, 1, 0.36, 1] as const;
function resolveTone(index: number, color?: string, activeColor?: string) {
return color
? {
color,
activeColor: activeColor ?? `color-mix(in srgb, ${color} 82%, black)`,
}
: CHART_PALETTE[index % CHART_PALETTE.length];
}
export function formatTokens(value: number): string {
const format = (number: number) =>
Number.isInteger(number) ? String(number) : number.toFixed(1);
if (value >= 1_000_000) return `${format(value / 1_000_000)}M`;
if (value >= 1_000) return `${format(value / 1_000)}k`;
return String(value);
}
function ProgressTrack({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
className={cx(
"flex h-1.5 w-full gap-px overflow-hidden rounded-full bg-chart-track",
className,
)}
>
{children}
</div>
);
}
export function AgentLimitsCard({
context,
plan = "Max (5x)",
planHref,
limits,
defaultExpanded = false,
onExpandedChange,
className,
}: AgentLimitsCardProps) {
const [expanded, setExpanded] = useState(defaultExpanded);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const usedTokens = context.segments
.filter((segment) => !segment.deferred)
.reduce((total, segment) => total + segment.tokens, 0);
const freeTokens = Math.max(0, context.max - usedTokens);
const percentage = (tokens: number) =>
(tokens / Math.max(1, context.max)) * 100;
const tones = context.segments.map((segment, index) =>
resolveTone(index + 1, segment.color),
);
const toggleGroup = (label: string) => {
setExpandedGroups((current) => {
const next = new Set(current);
if (next.has(label)) next.delete(label);
else next.add(label);
return next;
});
};
return (
<section
className={cx(
"flex w-full min-w-0 flex-col rounded-2xl bg-background-secondary-default px-4 pt-2.5 pb-4",
className,
)}
>
<button
type="button"
onClick={() => {
const next = !expanded;
setExpanded(next);
onExpandedChange?.(next);
}}
aria-expanded={expanded}
className="group -mx-2 flex cursor-pointer items-center justify-between gap-3 rounded-2lg px-2 py-1 text-left outline-none transition-colors duration-150 hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<span className="text-body-medium text-text-secondary">
Context window
</span>
<span className="flex items-center gap-2">
<span className="text-body-medium whitespace-nowrap text-text-secondary tabular-nums">
{formatTokens(usedTokens)} / {formatTokens(context.max)}{" "}
<span className="text-text-primary">
({Math.round(percentage(usedTokens))}%)
</span>
</span>
<RiArrowDownSLine
className={cx(
"size-4 shrink-0 text-text-tertiary transition-transform duration-200 ease-out group-hover:text-text-secondary",
expanded && "rotate-180",
)}
aria-hidden
/>
</span>
</button>
<ProgressTrack className="mt-1.5">
{context.segments.map((segment, index) =>
segment.deferred ? null : (
<div
key={segment.label}
className="h-full shrink-0 transition-[width] duration-500 ease-out"
style={{
width: `${percentage(segment.tokens)}%`,
backgroundColor: tones[index].color,
}}
title={`${segment.label} · ${formatTokens(segment.tokens)}`}
/>
),
)}
</ProgressTrack>
<AnimatePresence initial={false}>
{expanded && (
<motion.div
key="breakdown"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.32, ease: EXPAND_EASING }}
className="-mx-2 overflow-hidden px-2"
>
<div className="flex flex-col pt-3">
{context.segments.map((segment, index) => (
<div
key={segment.label}
className="flex items-center gap-2 py-[5px]"
>
<span
className="size-2.5 shrink-0 rounded-[3px]"
style={{
backgroundColor: segment.deferred
? "var(--color-chart-cursor)"
: tones[index].color,
}}
/>
<span className="min-w-0 flex-1 truncate text-body-regular text-text-primary">
{segment.label}
</span>
<span className="text-body-regular text-text-tertiary tabular-nums">
{formatTokens(segment.tokens)}
</span>
<span className="w-14 text-right text-body-medium text-text-primary tabular-nums">
{segment.deferred
? "—"
: `${percentage(segment.tokens).toFixed(1)}%`}
</span>
</div>
))}
<div className="flex items-center gap-2 py-[5px]">
<span className="size-2.5 shrink-0 rounded-[3px] bg-chart-track" />
<span className="min-w-0 flex-1 truncate text-body-regular text-text-primary">
Free space
</span>
<span className="text-body-regular text-text-tertiary tabular-nums">
{formatTokens(freeTokens)}
</span>
<span className="w-14 text-right text-body-medium text-text-primary tabular-nums">
{percentage(freeTokens).toFixed(1)}%
</span>
</div>
{context.groups && context.groups.length > 0 && (
<div className="mt-1.5 flex flex-col">
{context.groups.map((group) => {
const groupExpanded = expandedGroups.has(group.label);
return (
<div key={group.label} className="flex flex-col">
<button
type="button"
onClick={() => toggleGroup(group.label)}
aria-expanded={groupExpanded}
className="-mx-2 flex cursor-pointer items-center gap-1.5 rounded-2lg px-2 py-[7px] text-left outline-none transition-colors duration-150 hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<RiArrowRightSLine
className={cx(
"size-4 shrink-0 text-text-tertiary transition-transform duration-200 ease-out",
groupExpanded && "rotate-90",
)}
aria-hidden
/>
<span className="min-w-0 flex-1 truncate text-body-regular text-text-secondary">
{group.label}
</span>
<span className="text-body-regular text-text-tertiary tabular-nums">
{formatTokens(group.tokens)}
</span>
<span className="w-14 text-right text-body-regular text-text-tertiary tabular-nums">
{group.items.length}
</span>
</button>
<AnimatePresence initial={false}>
{groupExpanded && (
<motion.div
key="items"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
duration: 0.26,
ease: EXPAND_EASING,
}}
className="overflow-hidden"
>
<div className="flex flex-col pb-1 pl-[22px]">
{group.items.map((item) => (
<div
key={item.label}
className="flex items-center gap-2 py-1"
>
<span className="min-w-0 flex-1 truncate text-body-2-regular text-text-secondary">
{item.label}
</span>
<span className="text-body-2-regular text-text-tertiary tabular-nums">
{formatTokens(item.tokens)}
</span>
<span className="w-14" />
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="my-3 h-px w-full bg-separator-border-strong" />
<div className="flex items-center justify-between gap-3 py-1">
<span className="text-body-medium text-text-secondary">
Plan usage limits{plan ? ` · ${plan}` : ""}
</span>
{planHref && (
<a
href={planHref}
aria-label="Manage plan"
className="flex size-6 items-center justify-center rounded-md text-text-tertiary outline-none transition-colors duration-150 hover:bg-background-secondary-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<RiArrowRightLine className="size-4" aria-hidden />
</a>
)}
</div>
<div className="flex flex-col gap-3 pt-1">
{limits.map((limit) => {
const usedPercentage = Math.round(
100 * Math.max(0, Math.min(1, limit.used)),
);
return (
<div key={limit.label} className="flex flex-col gap-1.5">
<div className="flex items-baseline justify-between gap-3">
<span className="min-w-0 truncate text-body-medium text-text-primary">
{limit.label}
</span>
<span className="flex shrink-0 items-baseline gap-2">
<span className="text-body-regular text-text-tertiary">
{limit.resets}
</span>
<span className="w-9 text-right text-body-medium text-text-primary tabular-nums">
{usedPercentage}%
</span>
</span>
</div>
<ProgressTrack>
<div
className="h-full rounded-full transition-[width] duration-500 ease-out"
style={{
width: `${usedPercentage}%`,
backgroundColor: "var(--color-chart-6)",
}}
/>
</ProgressTrack>
</div>
);
})}
</div>
</section>
);
}"use client";
import { useState } from "react";
import {
RiArrowDownSLine,
RiArrowRightLine,
RiArrowRightSLine,
} from "@remixicon/react";
import { AnimatePresence, motion } from "motion/react";
import { cx } from "@/utils/cx";
export type AgentLimitSegment = {
label: string;
tokens: number;
/** Optional CSS color. Falls back to the BoardCN chart palette. */
color?: string;
/** Deferred segments appear in the breakdown but do not count toward usage. */
deferred?: boolean;
};
export type AgentLimitGroupItem = {
label: string;
tokens: number;
};
export type AgentLimitGroup = {
label: string;
tokens: number;
items: readonly AgentLimitGroupItem[];
};
export type AgentContextLimit = {
max: number;
segments: readonly AgentLimitSegment[];
groups?: readonly AgentLimitGroup[];
};
export type AgentPlanLimit = {
label: string;
/** Used capacity expressed as a ratio from 0 to 1. */
used: number;
resets: string;
};
export type AgentLimitsCardProps = {
context: AgentContextLimit;
plan?: string;
planHref?: string;
limits: readonly AgentPlanLimit[];
defaultExpanded?: boolean;
onExpandedChange?: (open: boolean) => void;
className?: string;
};
const CHART_PALETTE = [2, 6, 5, 3, 8, 7, 4, 1].map((tone) => ({
color: `var(--color-chart-${tone})`,
activeColor: `var(--color-chart-${tone}-active)`,
}));
const EXPAND_EASING = [0.22, 1, 0.36, 1] as const;
function resolveTone(index: number, color?: string, activeColor?: string) {
return color
? {
color,
activeColor: activeColor ?? `color-mix(in srgb, ${color} 82%, black)`,
}
: CHART_PALETTE[index % CHART_PALETTE.length];
}
export function formatTokens(value: number): string {
const format = (number: number) =>
Number.isInteger(number) ? String(number) : number.toFixed(1);
if (value >= 1_000_000) return `${format(value / 1_000_000)}M`;
if (value >= 1_000) return `${format(value / 1_000)}k`;
return String(value);
}
function ProgressTrack({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
className={cx(
"flex h-1.5 w-full gap-px overflow-hidden rounded-full bg-chart-track",
className,
)}
>
{children}
</div>
);
}
export function AgentLimitsCard({
context,
plan = "Max (5x)",
planHref,
limits,
defaultExpanded = false,
onExpandedChange,
className,
}: AgentLimitsCardProps) {
const [expanded, setExpanded] = useState(defaultExpanded);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const usedTokens = context.segments
.filter((segment) => !segment.deferred)
.reduce((total, segment) => total + segment.tokens, 0);
const freeTokens = Math.max(0, context.max - usedTokens);
const percentage = (tokens: number) =>
(tokens / Math.max(1, context.max)) * 100;
const tones = context.segments.map((segment, index) =>
resolveTone(index + 1, segment.color),
);
const toggleGroup = (label: string) => {
setExpandedGroups((current) => {
const next = new Set(current);
if (next.has(label)) next.delete(label);
else next.add(label);
return next;
});
};
return (
<section
className={cx(
"flex w-full min-w-0 flex-col rounded-2xl bg-background-secondary-default px-4 pt-2.5 pb-4",
className,
)}
>
<button
type="button"
onClick={() => {
const next = !expanded;
setExpanded(next);
onExpandedChange?.(next);
}}
aria-expanded={expanded}
className="group -mx-2 flex cursor-pointer items-center justify-between gap-3 rounded-2lg px-2 py-1 text-left outline-none transition-colors duration-150 hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<span className="text-body-medium text-text-secondary">
Context window
</span>
<span className="flex items-center gap-2">
<span className="text-body-medium whitespace-nowrap text-text-secondary tabular-nums">
{formatTokens(usedTokens)} / {formatTokens(context.max)}{" "}
<span className="text-text-primary">
({Math.round(percentage(usedTokens))}%)
</span>
</span>
<RiArrowDownSLine
className={cx(
"size-4 shrink-0 text-text-tertiary transition-transform duration-200 ease-out group-hover:text-text-secondary",
expanded && "rotate-180",
)}
aria-hidden
/>
</span>
</button>
<ProgressTrack className="mt-1.5">
{context.segments.map((segment, index) =>
segment.deferred ? null : (
<div
key={segment.label}
className="h-full shrink-0 transition-[width] duration-500 ease-out"
style={{
width: `${percentage(segment.tokens)}%`,
backgroundColor: tones[index].color,
}}
title={`${segment.label} · ${formatTokens(segment.tokens)}`}
/>
),
)}
</ProgressTrack>
<AnimatePresence initial={false}>
{expanded && (
<motion.div
key="breakdown"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.32, ease: EXPAND_EASING }}
className="-mx-2 overflow-hidden px-2"
>
<div className="flex flex-col pt-3">
{context.segments.map((segment, index) => (
<div
key={segment.label}
className="flex items-center gap-2 py-[5px]"
>
<span
className="size-2.5 shrink-0 rounded-[3px]"
style={{
backgroundColor: segment.deferred
? "var(--color-chart-cursor)"
: tones[index].color,
}}
/>
<span className="min-w-0 flex-1 truncate text-body-regular text-text-primary">
{segment.label}
</span>
<span className="text-body-regular text-text-tertiary tabular-nums">
{formatTokens(segment.tokens)}
</span>
<span className="w-14 text-right text-body-medium text-text-primary tabular-nums">
{segment.deferred
? "—"
: `${percentage(segment.tokens).toFixed(1)}%`}
</span>
</div>
))}
<div className="flex items-center gap-2 py-[5px]">
<span className="size-2.5 shrink-0 rounded-[3px] bg-chart-track" />
<span className="min-w-0 flex-1 truncate text-body-regular text-text-primary">
Free space
</span>
<span className="text-body-regular text-text-tertiary tabular-nums">
{formatTokens(freeTokens)}
</span>
<span className="w-14 text-right text-body-medium text-text-primary tabular-nums">
{percentage(freeTokens).toFixed(1)}%
</span>
</div>
{context.groups && context.groups.length > 0 && (
<div className="mt-1.5 flex flex-col">
{context.groups.map((group) => {
const groupExpanded = expandedGroups.has(group.label);
return (
<div key={group.label} className="flex flex-col">
<button
type="button"
onClick={() => toggleGroup(group.label)}
aria-expanded={groupExpanded}
className="-mx-2 flex cursor-pointer items-center gap-1.5 rounded-2lg px-2 py-[7px] text-left outline-none transition-colors duration-150 hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<RiArrowRightSLine
className={cx(
"size-4 shrink-0 text-text-tertiary transition-transform duration-200 ease-out",
groupExpanded && "rotate-90",
)}
aria-hidden
/>
<span className="min-w-0 flex-1 truncate text-body-regular text-text-secondary">
{group.label}
</span>
<span className="text-body-regular text-text-tertiary tabular-nums">
{formatTokens(group.tokens)}
</span>
<span className="w-14 text-right text-body-regular text-text-tertiary tabular-nums">
{group.items.length}
</span>
</button>
<AnimatePresence initial={false}>
{groupExpanded && (
<motion.div
key="items"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
duration: 0.26,
ease: EXPAND_EASING,
}}
className="overflow-hidden"
>
<div className="flex flex-col pb-1 pl-[22px]">
{group.items.map((item) => (
<div
key={item.label}
className="flex items-center gap-2 py-1"
>
<span className="min-w-0 flex-1 truncate text-body-2-regular text-text-secondary">
{item.label}
</span>
<span className="text-body-2-regular text-text-tertiary tabular-nums">
{formatTokens(item.tokens)}
</span>
<span className="w-14" />
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="my-3 h-px w-full bg-separator-border-strong" />
<div className="flex items-center justify-between gap-3 py-1">
<span className="text-body-medium text-text-secondary">
Plan usage limits{plan ? ` · ${plan}` : ""}
</span>
{planHref && (
<a
href={planHref}
aria-label="Manage plan"
className="flex size-6 items-center justify-center rounded-md text-text-tertiary outline-none transition-colors duration-150 hover:bg-background-secondary-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-border-focus-ring"
>
<RiArrowRightLine className="size-4" aria-hidden />
</a>
)}
</div>
<div className="flex flex-col gap-3 pt-1">
{limits.map((limit) => {
const usedPercentage = Math.round(
100 * Math.max(0, Math.min(1, limit.used)),
);
return (
<div key={limit.label} className="flex flex-col gap-1.5">
<div className="flex items-baseline justify-between gap-3">
<span className="min-w-0 truncate text-body-medium text-text-primary">
{limit.label}
</span>
<span className="flex shrink-0 items-baseline gap-2">
<span className="text-body-regular text-text-tertiary">
{limit.resets}
</span>
<span className="w-9 text-right text-body-medium text-text-primary tabular-nums">
{usedPercentage}%
</span>
</span>
</div>
<ProgressTrack>
<div
className="h-full rounded-full transition-[width] duration-500 ease-out"
style={{
width: `${usedPercentage}%`,
backgroundColor: "var(--color-chart-6)",
}}
/>
</ProgressTrack>
</div>
);
})}
</div>
</section>
);
}Props
Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.
AgentLimitsCard
| Prop | Type | Default | Description |
|---|---|---|---|
| contextrequired | AgentContextLimit | — | — |
| limitsrequired | readonly AgentPlanLimit[] | — | — |
| className | string | — | — |
| defaultExpanded | boolean | false | — |
| onExpandedChange | (open: boolean) => void | — | — |
| plan | string | Max (5x) | — |
| planHref | string | — | — |