Skip to main content

ns-ui

Meter Context Window

A context-window budget meter: one hairline stacked bar segmenting the window into system prompt / tools / history / current turn, told apart by fill pattern instead of color, that visibly resettles rather than jumps when the window compacts.

Use when a read-only budget readout for an already-composed context window, splitting it into system prompt, tools, history and current turn by fill pattern rather than color so the shares survive a monochrome or colorblind read, and resettling rather than jumping when a compaction lands; pick context-prompt-shims instead when the user decides what goes in and in what order, or context-compaction-river when the interesting thing is which turns were compacted away and whether they can come back.

Install

npx shadcn add https://design.helpmarq.com/r/meter-context-window.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Source
registry/core/meter-context-window/component.tsx
"use client";

// A context-window budget meter: one hairline stacked bar segmenting the
// window into system prompt / tools / conversation history / current turn,
// plus the unclaimed remainder, with a Geist Mono token readout underneath.
// It reads at rest because the bar IS the thing it describes — no legend
// needed to decode "what does 40% mean," the segment widths are the answer.
//
// Segments carry no color of their own (the palette is monochrome by house
// rule) — they're told apart by a stepped weight ramp (four flat, clearly
// separated color-mix() tones, lightest for system up to heaviest for the
// live turn) plus a bonus pattern once a segment is wide enough to resolve
// it: tools a diagonal hatch, history a stipple, current turn a fine
// vertical hatch that breathes gently while it's live. A 1px hairline
// divider sits between every pair so the boundary reads even when two
// tones land close together. Pure CSS, built from color-mix() against
// --foreground and --background, never a hex literal and never --ns-accent
// (accent is reserved for interaction, not decoration).
//
// Layout is plain flexbox: each segment's flex-basis is its share of
// max(capacity, used) as a percentage, with a min-width floor on any segment
// that has real tokens but would round below a legible sliver. The floor is
// enforced by the browser's own flex-shrink algorithm (larger segments give
// up the room), not by a hand-rolled pixel solver — a segment with tokens
// never disappears, one with none never fakes a sliver it didn't earn.
//
// Approaching capacity is signalled through form: the wrapper gains a
// pulsing inset ring, the readout goes bold and grows a triangle glyph, and
// the free segment (if any width remains) grows a hazard hatch — never a red
// fill, this palette doesn't have one. Compaction is just a prop change:
// react re-renders new percentages and the segments ease into them on a
// shared transition, so the resettle itself carries the news.
//
// All the numbers are real DOM text (a <p> readout plus a legend list), so
// assistive tech gets the actual figures regardless of what the bar looks
// like; the bar itself is aria-hidden as a redundant visual. The readout is
// a polite live region since it changes asynchronously as tokens accrue.
// prefers-reduced-motion drops the transition, the breathing, and the
// hazard pulse — every state stays fully legible, just static.

export interface BallastContextProps {
  /** total size of the context window, in tokens */
  capacity: number;
  /** tokens spent on the system prompt */
  system: number;
  /** tokens spent on tool/function definitions and results */
  tools: number;
  /** tokens spent on prior conversation turns */
  history: number;
  /** tokens spent on the turn currently being read or generated */
  turn: number;
  /**
   * fraction of capacity (0-1) below which the free segment is flagged as
   * low headroom. default 0.08 (8%)
   */
  lowFreeThreshold?: number;
  /** accessible name for the meter. default "Context window budget" */
  ariaLabel?: string;
  /** extra classes merged onto the rendered root element */
  className?: string;
}

const ROLES = [
  { role: "system", label: "System" },
  { role: "tools", label: "Tools" },
  { role: "history", label: "History" },
  { role: "turn", label: "Turn" },
  { role: "free", label: "Free" },
] as const;

function formatTokens(n: number): string {
  return Math.round(Math.max(0, n)).toLocaleString("en-US");
}

function formatPct(tokens: number, denom: number): string {
  if (tokens <= 0 || denom <= 0) return "0%";
  const p = (tokens / denom) * 100;
  if (p < 1) return "<1%";
  return `${Math.round(p)}%`;
}

export function BallastContext({
  capacity,
  system,
  tools,
  history,
  turn,
  lowFreeThreshold = 0.08,
  ariaLabel = "Context window budget",
  className = "",
}: BallastContextProps) {
  const safeSystem = Math.max(0, system);
  const safeTools = Math.max(0, tools);
  const safeHistory = Math.max(0, history);
  const safeTurn = Math.max(0, turn);
  const safeCapacity = Math.max(0, capacity);

  const used = safeSystem + safeTools + safeHistory + safeTurn;
  const overBudget = safeCapacity > 0 && used > safeCapacity;
  const free = Math.max(0, safeCapacity - used);
  // denominator the bar fills to 100% against: capacity normally, or the
  // used total itself once over budget, so the four segments still sum to
  // a full bar instead of visually shrinking below their real weight
  const denom = Math.max(safeCapacity, used, 1);

  const freeFraction = safeCapacity > 0 ? free / safeCapacity : 0;
  const warn = safeCapacity > 0 && (overBudget || freeFraction <= lowFreeThreshold);

  const tokensByRole: Record<(typeof ROLES)[number]["role"], number> = {
    system: safeSystem,
    tools: safeTools,
    history: safeHistory,
    turn: safeTurn,
    free,
  };

  const pct = safeCapacity > 0 ? Math.round((used / safeCapacity) * 100) : 0;
  const statusSuffix = overBudget ? " · over budget" : warn ? " · low headroom" : "";

  return (
    <div
      role="group"
      aria-label={ariaLabel}
      data-warn={warn}
      className={["ns-ballast", className].join(" ")}
    >
      <style>{`
.ns-ballast-track{transition:box-shadow 300ms ease-out}
.ns-ballast[data-warn="true"] .ns-ballast-track{animation:ns-ballast-ring 1.8s ease-in-out infinite}
.ns-ballast-seg{transition:flex-basis 520ms cubic-bezier(.22,1,.36,1)}
.ns-ballast-track .ns-ballast-seg:not([data-role="free"]){border-right:1px solid var(--background)}
.ns-ballast-seg[data-role="system"]{background:color-mix(in srgb, var(--foreground) 22%, var(--background))}
.ns-ballast-seg[data-role="tools"]{background-color:color-mix(in srgb, var(--foreground) 30%, var(--background));background-image:repeating-linear-gradient(45deg, color-mix(in srgb, var(--foreground) 46%, var(--background)) 0 2px, transparent 2px 5px)}
.ns-ballast-seg[data-role="history"]{background-color:color-mix(in srgb, var(--foreground) 40%, var(--background));background-image:radial-gradient(color-mix(in srgb, var(--foreground) 60%, var(--background)) 1.1px, transparent 1.3px);background-size:5px 5px}
.ns-ballast-seg[data-role="turn"]{background-color:color-mix(in srgb, var(--foreground) 52%, var(--background));background-image:repeating-linear-gradient(90deg, color-mix(in srgb, var(--foreground) 72%, var(--background)) 0 1.5px, transparent 1.5px 4px);animation:ns-ballast-breathe 2.6s ease-in-out infinite}
.ns-ballast-seg[data-role="free"]{background:transparent}
.ns-ballast[data-warn="true"] .ns-ballast-seg[data-role="free"]{background-image:repeating-linear-gradient(135deg, color-mix(in srgb, var(--foreground) 26%, transparent) 0 1px, transparent 1px 6px);animation:ns-ballast-hazard 1.6s ease-in-out infinite}
@keyframes ns-ballast-breathe{0%,100%{opacity:.82}50%{opacity:1}}
@keyframes ns-ballast-hazard{0%,100%{opacity:.5}50%{opacity:1}}
@keyframes ns-ballast-ring{0%,100%{box-shadow:inset 0 0 0 1px var(--border)}50%{box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--foreground) 45%, transparent)}}
@media (prefers-reduced-motion: reduce){
  .ns-ballast-seg{transition:none;animation:none !important;opacity:1 !important}
  .ns-ballast-track{animation:none !important;box-shadow:inset 0 0 0 1px var(--border) !important}
}
`}</style>

      <div
        aria-hidden="true"
        className="ns-ballast-track flex h-2.5 w-full overflow-hidden rounded-full border border-border bg-background"
      >
        {ROLES.map(({ role }) => {
          const t = tokensByRole[role];
          return (
            <div
              key={role}
              data-role={role}
              className="ns-ballast-seg h-full shrink"
              style={{
                flexBasis: `${(t / denom) * 100}%`,
                flexGrow: 0,
                minWidth: t > 0 ? "3px" : "0px",
              }}
            />
          );
        })}
      </div>

      <p
        aria-live="polite"
        className={[
          "mt-3 flex items-center gap-1.5 font-mono text-xs tabular-nums text-ns-muted",
          warn ? "font-semibold text-foreground" : "",
        ].join(" ")}
      >
        {warn ? (
          <svg
            aria-hidden="true"
            width="12"
            height="12"
            viewBox="0 0 16 16"
            fill="none"
            className="shrink-0 text-foreground"
          >
            <path
              d="M8 2.4 14.6 13.6H1.4L8 2.4Z"
              stroke="currentColor"
              strokeWidth="1.3"
              strokeLinejoin="round"
            />
            <path d="M8 6.6v3.4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
            <circle cx="8" cy="11.9" r="0.85" fill="currentColor" />
          </svg>
        ) : null}
        <span>
          {formatTokens(used)} / {formatTokens(safeCapacity)} tokens · {pct}%{statusSuffix}
        </span>
      </p>

      <ul className="mt-2.5 flex flex-wrap gap-x-4 gap-y-1.5 font-mono text-[11px] tabular-nums text-ns-muted">
        {ROLES.map(({ role, label }) => {
          const t = tokensByRole[role];
          return (
            <li key={role} className="flex items-center gap-1.5">
              <span
                aria-hidden="true"
                data-role={role}
                className="ns-ballast-seg h-2.5 w-2.5 shrink-0 rounded-[2px] border border-border"
              />
              <span className="tracking-wide text-ns-muted">{label}</span>
              <span className="text-foreground">{formatTokens(t)}</span>
              <span>{formatPct(t, denom)}</span>
            </li>
          );
        })}
      </ul>
    </div>
  );
}
Build spec

A context-window budget meter for agent UIs: one hairline stacked bar (rounded-full, 10px tall, 1px --border ring, bg-background bed) segmenting the window into five shares — system prompt, tools, conversation history, current turn, and the unclaimed remainder — plus a font-mono tabular-nums readout and a text legend underneath. Pure DOM/CSS, no canvas, no SVG paint loop, no rAF: each segment is a flex child whose flex-basis is its percentage of max(capacity, used) so the five always sum to a full bar even when usage exceeds capacity, and a min-width floor (3px) on any segment that has real tokens keeps it from vanishing below a legible sliver — the browser's own flex-shrink gives it room by shrinking the larger segments, not a hand-rolled pixel solver, so a segment with zero tokens renders zero width honestly and one with tokens never fakes a mark it didn't earn. The palette is monochrome by house rule, so segments are told apart primarily by a stepped weight ramp (four flat, unmistakably different color-mix() tones against --foreground, never --ns-accent — 22/30/40/52% — so even a 3px sliver reads as a distinct step, with no fill ever falling through to a bare or transparent look that would misread as free space) plus a bonus pattern layered on top of each base tone once a segment is wide enough to resolve it (system stays flat, tools gets a 45deg diagonal hatch, history a stipple, current turn a close vertical hatch that breathes gently — a 2.6s opacity pulse — while it's the live, growing share); a hairline 1px --background divider sits between every adjacent pair so boundaries stay crisp even when two neighboring tones land close together, and the free remainder is bare track. Compaction is just a prop change — react re-renders new percentages and every segment eases into them on a shared 520ms cubic-bezier(0.22,1,0.36,1) flex-basis transition, so the resettle itself is what communicates that compaction happened, never an instant jump. Approaching capacity reads as a warning through form, never a red fill: once free space drops to or below `lowFreeThreshold` (prop, fraction of capacity, default 0.08) or usage exceeds capacity outright, the track gains a slow pulsing inset ring, the readout goes bold and grows a small triangle glyph, and the free segment (if any width remains) grows a diagonal hazard hatch. Props: capacity, system, tools, history, turn (all plain numbers, tokens, fully controlled — the component holds no state of its own), lowFreeThreshold, ariaLabel, className. Accessibility: the bar itself is aria-hidden (a redundant visual against the text that follows); a role=group wraps the whole meter with an accessible name; the token readout is a real aria-live=polite paragraph so async updates get announced; the legend below the bar is plain DOM text — label, exact token count, and a percentage (values under 1% but above zero read '<1%' rather than rounding to a misleading 0%) — for every one of the five shares, so assistive tech gets the real numbers regardless of what the bar renders. The component exposes no interactive controls of its own (display-only, correctly skipped by the tab-reachability check) and renders correctly in both themes since every ink is a CSS custom property, never a literal. prefers-reduced-motion drops the flex-basis transition, the current-turn breathing, and the hazard pulse entirely — every state (empty, mid-fill, near-capacity, over-budget) stays fully legible, just static. Demo: an agent-session card (claude-4.5, 200K-token window) seeded with a realistic mid-fill baseline so the resting screenshot already shows the meter doing its job, plus a live status line, a CALL TOOL button that grows the tools share, a COMPACT CONTEXT button that shrinks history to roughly a ninth of itself and lets the resettle play, a RESET SESSION button, a FILL WINDOW button that jumps usage straight to ~96% so the near-capacity warning state (pulsing ring, bold readout + triangle glyph, hazard hatch on the free sliver) is reachable in one click rather than only after minutes of ambient growth, and a background interval that streams small turns into the current-turn share and folds each one into history after it settles — pausing near 94% usage the way a real agent would compact before running out, so the near-capacity warning state is also reachable just by leaving the demo running.

Props

PropTypeDefaultDescription
capacitynumbertotal size of the context window, in tokens
systemnumbertokens spent on the system prompt
toolsnumbertokens spent on tool/function definitions and results
historynumbertokens spent on prior conversation turns
turnnumbertokens spent on the turn currently being read or generated
lowFreeThreshold?number0.08fraction of capacity (0-1) below which the free segment is flagged as low headroom. default 0.08 (8%)
ariaLabel?string"Context window budget"accessible name for the meter. default "Context window budget"
className?stringextra classes merged onto the rendered root element