A token-usage meter for an LLM context window, segmented by source, that fills on view and shifts to a warning tone near the limit with tabular figures.
npx shadcn@latest add @paragon/context-window-meter"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
export interface ContextSegment {
label: string;
tokens: number;
/** CSS color. Defaults cycle through a restrained palette. */
color?: string;
}
export interface ContextWindowMeterProps extends React.ComponentProps<"div"> {
/** Segments filling the window, in stack order. */
segments: ContextSegment[];
/** Total window size in tokens. */
max: number;
/** Fraction (0–1) at which the meter shifts to a warning tone. */
warnAt?: number;
static?: boolean;
}
const DEFAULT_COLORS = [
"oklch(0.585 0.17 260)",
"oklch(0.68 0.13 165)",
"oklch(0.76 0.13 85)",
];
function formatTokens(n: number): string {
if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`;
return `${n}`;
}
/**
* A token-usage meter for an LLM context window, segmented by source
* (system / history / response, etc.). On first view each segment grows from
* zero width via a scaleX transform (transform-origin left, so no layout
* thrash), staggered left-to-right. Near the configured limit the used figure
* and remaining segment shift to a warning tone. All figures are tabular.
* Reduced motion renders the filled state immediately.
*/
export function ContextWindowMeter({
segments,
max,
warnAt = 0.85,
static: isStatic = false,
className,
...props
}: ContextWindowMeterProps) {
const ref = React.useRef<HTMLDivElement>(null);
const reducedMotion = useReducedMotion();
const inView = useInView(ref, { once: true, margin: "0px 0px -24px 0px" });
const animate = !isStatic && !reducedMotion;
const filled = !animate || inView;
const used = segments.reduce((sum, s) => sum + s.tokens, 0);
const usedFrac = Math.min(1, used / max);
const warning = usedFrac >= warnAt;
const remaining = Math.max(0, max - used);
return (
<div
ref={ref}
data-slot="context-window-meter"
className={cn(
"flex w-full max-w-md flex-col gap-2.5 rounded-xl bg-card p-4 shadow-border",
className,
)}
{...props}
>
<div className="flex items-baseline justify-between gap-3">
<span className="text-sm font-medium text-foreground">
Context window
</span>
<span className="flex items-baseline gap-1 text-xs tabular-nums">
<span
className={cn(
"font-semibold",
warning ? "text-warning" : "text-foreground",
)}
style={{
transition: isStatic ? undefined : "color 250ms var(--ease-out)",
}}
>
{used.toLocaleString("en-US")}
</span>
<span className="text-muted-foreground">
/ {max.toLocaleString("en-US")} tokens
</span>
</span>
</div>
<div
role="meter"
aria-valuemin={0}
aria-valuemax={max}
aria-valuenow={used}
aria-valuetext={`${used.toLocaleString("en-US")} of ${max.toLocaleString("en-US")} tokens used`}
className="flex h-2.5 w-full overflow-hidden rounded-full bg-secondary"
>
{segments.map((seg, i) => {
const frac = seg.tokens / max;
return (
<div
key={seg.label}
className="h-full origin-left"
style={{
width: `${frac * 100}%`,
background:
seg.color ?? DEFAULT_COLORS[i % DEFAULT_COLORS.length],
transform: filled ? "scaleX(1)" : "scaleX(0)",
transition: animate
? `transform 560ms var(--ease-out) ${i * 90}ms`
: undefined,
}}
/>
);
})}
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
{segments.map((seg, i) => (
<div
key={seg.label}
className="flex items-center gap-1.5 text-[11px] text-muted-foreground"
>
<span
aria-hidden
className="size-2 rounded-full"
style={{
background: seg.color ?? DEFAULT_COLORS[i % DEFAULT_COLORS.length],
}}
/>
{seg.label}
<span className="tabular-nums text-muted-foreground/70">
{formatTokens(seg.tokens)}
</span>
</div>
))}
<span
className={cn(
"ml-auto text-[11px] tabular-nums",
warning ? "text-warning" : "text-muted-foreground",
)}
>
{formatTokens(remaining)} left
</span>
</div>
</div>
);
}