Context Meter
A token-budget meter for AI apps: one bar of coloured segments, a big total and chips that isolate each source.
npx shadcn@latest add https://sundaeui.dev/r/context-meter.json'use client'
import { useId, useState } from 'react'
import './context-meter.css'
export interface ContextSegment {
label: string
value: number
color: string
}
export interface ContextMeterProps {
segments: ContextSegment[]
/** Window size in tokens. */
limit: number
title?: string
variant?: 'detailed' | 'compact'
fill?: 'dark' | 'light'
stroke?: boolean
/** Externally highlighted segment (for demos). */
highlight?: string | null
className?: string
}
const fmt = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(n >= 100_000 ? 0 : 1)}K` : String(n))
/** A token-budget meter: one bar of coloured segments, a big total and chips that isolate a source. */
export function ContextMeter({ segments, limit, title = 'Context', variant: initial = 'detailed', fill = 'dark', stroke = true, highlight, className }: ContextMeterProps) {
const [variant, setVariant] = useState(initial)
const [picked, setPicked] = useState<string | null>(null)
const id = useId()
const used = segments.reduce((s, x) => s + x.value, 0)
const active = picked ?? highlight ?? null
const shown = active ? segments.find((s) => s.label === active)?.value ?? used : used
const pct = Math.round((used / limit) * 100)
return (
<section className={['sd-cm', className].filter(Boolean).join(' ')} data-fill={fill} data-stroke={stroke || undefined} data-variant={variant} aria-labelledby={`${id}-t`}>
<header className="sd-cm__head">
<h3 id={`${id}-t`}>{title}</h3>
<button type="button" className="sd-cm__mode" onClick={() => setVariant(variant === 'detailed' ? 'compact' : 'detailed')} aria-pressed={variant === 'compact'}>
{variant === 'detailed' ? 'Compact' : 'Details'}
</button>
</header>
<p className="sd-cm__total">
<span className="sd-cm__num">{fmt(shown)}</span>
<span className="sd-cm__of">{active ? active : `of ${fmt(limit)}`}</span>
</p>
<div className="sd-cm__bar" role="meter" aria-label={`${title} used`} aria-valuemin={0} aria-valuemax={limit} aria-valuenow={used} aria-valuetext={`${fmt(used)} of ${fmt(limit)} tokens, ${pct}%`}>
{segments.map((s) => (
<span key={s.label} className="sd-cm__seg" data-dim={(active && active !== s.label) || undefined} style={{ flexGrow: s.value, flexBasis: 0, background: s.color }} />
))}
<span className="sd-cm__free" style={{ flexGrow: Math.max(0, limit - used), flexBasis: 0 }} />
</div>
<ul className="sd-cm__chips" aria-label="Sources">
{segments.map((s) => (
<li key={s.label}>
<button
type="button"
aria-pressed={active === s.label}
onClick={() => setPicked(picked === s.label ? null : s.label)}
onPointerEnter={() => setPicked(s.label)}
onPointerLeave={() => setPicked(null)}
className="sd-cm__chip"
>
<i style={{ background: s.color }} aria-hidden />
{s.label}
{variant === 'detailed' && <span className="sd-cm__v">{fmt(s.value)}</span>}
</button>
</li>
))}
</ul>
</section>
)
}.sd-cm { --bg: #1d1d1d; --fg: #f7f7f7; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.09); --soft: rgb(255 255 255 / 0.07);
width: min(100%, 320px); padding: 18px; border-radius: 22px; background: var(--bg); color: var(--fg); font-feature-settings: 'tnum'; }
.sd-cm[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.08); --soft: rgb(0 0 0 / 0.05); }
.sd-cm[data-stroke] { box-shadow: inset 0 0 0 1px var(--line); }
.sd-cm__head { display: flex; align-items: center; justify-content: space-between; }
.sd-cm__head h3 { margin: 0; font-size: 15px; font-weight: 500; }
.sd-cm__mode { height: 28px; padding: 0 12px; border-radius: 999px; background: var(--fg); color: var(--bg); font-size: 12.5px; font-weight: 500; transition: transform 200ms; }
.sd-cm__mode:active { transform: scale(0.94); }
.sd-cm__total { display: flex; align-items: baseline; gap: 8px; margin: 14px 0 12px; }
.sd-cm__num { font-size: 40px; font-weight: 600; letter-spacing: -0.04em; line-height: 1; }
.sd-cm__of { font-size: 13px; color: var(--mute); }
.sd-cm__bar { display: flex; gap: 3px; height: 30px; }
.sd-cm[data-variant='compact'] .sd-cm__bar { height: 14px; }
.sd-cm__seg, .sd-cm__free { border-radius: 7px; transition: flex-grow 700ms cubic-bezier(0.2, 1, 0.3, 1), opacity 250ms, height 400ms; min-width: 4px; }
.sd-cm__seg[data-dim] { opacity: 0.22; }
.sd-cm__free { background: repeating-linear-gradient(135deg, var(--soft) 0 4px, transparent 4px 8px); box-shadow: inset 0 0 0 1px var(--line); }
.sd-cm__chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 0; padding: 0; list-style: none; }
.sd-cm__chip { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border-radius: 999px; background: var(--soft); color: var(--fg); font-size: 12.5px; transition: background-color 200ms, box-shadow 200ms; }
.sd-cm__chip[aria-pressed='true'] { box-shadow: inset 0 0 0 1px var(--fg); }
.sd-cm__chip i { width: 8px; height: 8px; border-radius: 3px; }
.sd-cm__v { color: var(--mute); }
.sd-cm button:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .sd-cm * { transition: none !important; } }Overview
Shows how much of a model’s context window is used and by what. Each source (chat, files, tools, system prompt, memory) is a segment of one bar, with free space hatched at the end.
Hover or press a chip and the other segments dim while the total switches to that source. A Compact toggle thins the bar and hides per-source numbers.
When to use
- Chat and agent UIs that need to show how close a conversation is to its context limit.
- Billing or usage pages that break one quota into a few sources.
When not to use
- More than six sources: segments get too thin to read. Use a table.
- Values that are not parts of one whole.
Installation
pnpm dlx shadcn@latest add https://sundaeui.dev/r/context-meter.jsonnpx shadcn@latest add https://sundaeui.dev/r/context-meter.jsonyarn dlx shadcn@latest add https://sundaeui.dev/r/context-meter.jsonbunx --bun shadcn@latest add https://sundaeui.dev/r/context-meter.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy context-meter.tsx and context-meter.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { ContextMeter } from '@/components/sundae/context-meter'
<ContextMeter
limit={200000}
segments={[
{ label: 'Chat', value: 41200, color: '#22d3ee' },
{ label: 'Files', value: 33800, color: '#6366f1' },
{ label: 'Tools', value: 18600, color: '#f472b6' },
]}
/>Props
| Prop | Type | Default |
|---|---|---|
segments*Label, token count and colour per source. | ContextSegment[] | — |
limit*Context window size in tokens. | number | — |
titleHeading. | string | 'Context' |
variantInitial density; users can toggle it. | 'detailed' | 'compact' | 'detailed' |
strokeHairline border. | boolean | true |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- The bar is role="meter" with aria-valuenow, aria-valuemax and a readable aria-valuetext ("128K of 200K tokens, 64%").
- Chips are toggle buttons (aria-pressed) so keyboard users can isolate a source.
- Reduced motion removes the segment width animation.
Customization
- Every colour is a CSS custom property on the root class; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
Keyboard
- Tab
- Move between the mode toggle and source chips
- Enter / Space
- Isolate or release a source
FAQ
- Is Context Meter free?
- Yes. Context Meter is free for personal and commercial projects, with full source on this page.
- How do I install Context Meter?
- Run npx shadcn@latest add https://sundaeui.dev/r/context-meter.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Context Meter support keyboard and screen readers?
- The bar is role="meter" with aria-valuenow, aria-valuemax and a readable aria-valuetext ("128K of 200K tokens, 64%"). Chips are toggle buttons (aria-pressed) so keyboard users can isolate a source.
- Does Context Meter respect reduced motion?
- Reduced motion removes the segment width animation.
More like this
All components →- Pro
Voice Orb
Components · Futuristic Agent Steps
Components · Understated- Pro
Reasoning Trace
Components · Refined - Pro
AI Composer
Components · Understated Setup Checklist
Components · UnderstatedDownload Button
Components · Refined