AI & Chat

Context Window Meter

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.

Install

npx shadcn@latest add @paragon/context-window-meter

context-window-meter.tsx

"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>
  );
}