AI

Token meter

Shows how much of a context window or usage budget is spent, split by what spent it.

Built on
role="meter"
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
2.4 kB
Dependencies
React
  • System12.4k
  • Messages96.3k
  • Files33.3k
  • Free58k

Plenty of room

Try its options

Loading its options.

Code
import { TokenMeter } from "@/components/equip/token-meter";

export default function Demo() {
  return (
    <TokenMeter
      style={{ width: 224 }}
      label="Context window"
      limit={200_000}
      segments={[
        { label: "System", value: 12_400 },
        { label: "Messages", value: 96_300 },
        { label: "Files", value: 33_300 },
      ]}
    />
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="width:224px" class="eq-token-meter" data-state="ok">
  <div class="eq-token-meter-head" aria-hidden="true">
    <span class="eq-token-meter-label">Context window</span>
    <span class="eq-token-meter-figure">142k of 200k</span>
  </div>
  <div class="eq-token-meter-bar" role="meter" aria-label="Context window" aria-valuemin="0" aria-valuemax="200000" aria-valuenow="142000" aria-valuetext="142k of 200k tokens used. Plenty of room.">
    <span class="eq-token-meter-segment" style="--_share:6.2%"></span>
    <span class="eq-token-meter-segment" style="--_share:48.15%"></span>
    <span class="eq-token-meter-segment" style="--_share:16.650000000000002%"></span>
  </div>
  <ul class="eq-token-meter-legend">
    <li class="eq-token-meter-item">
      <span class="eq-token-meter-swatch" aria-hidden="true"></span>
      <span class="eq-token-meter-name">System</span>
      <span class="eq-token-meter-amount">12.4k</span>
    </li>
    <li class="eq-token-meter-item">
      <span class="eq-token-meter-swatch" aria-hidden="true"></span>
      <span class="eq-token-meter-name">Messages</span>
      <span class="eq-token-meter-amount">96.3k</span>
    </li>
    <li class="eq-token-meter-item">
      <span class="eq-token-meter-swatch" aria-hidden="true"></span>
      <span class="eq-token-meter-name">Files</span>
      <span class="eq-token-meter-amount">33.3k</span>
    </li>
    <li class="eq-token-meter-item" data-free="">
      <span class="eq-token-meter-swatch" aria-hidden="true"></span>
      <span class="eq-token-meter-name">Free</span>
      <span class="eq-token-meter-amount">58k</span>
    </li>
  </ul>
  <p class="eq-token-meter-state">Plenty of room</p>
</div>

Examples

Getting full and over the limit

The state is written out in words, so it never depends on the colour of the dot.

  • System8.2k
  • Messages71.5k
  • Files29.8k
  • Free18.5k

Getting full

  • System8.2k
  • Messages94.1k
  • Files41.7k

Over the limit

Code
import { TokenMeter } from "@/components/equip/token-meter";

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 24, maxWidth: 360 }}>
      <TokenMeter
        label="Context window"
        limit={128_000}
        segments={[
          { label: "System", value: 8_200 },
          { label: "Messages", value: 71_500 },
          { label: "Files", value: 29_800 },
        ]}
      />
      <TokenMeter
        label="Context window"
        limit={128_000}
        segments={[
          { label: "System", value: 8_200 },
          { label: "Messages", value: 94_100 },
          { label: "Files", value: 41_700 },
        ]}
      />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-direction:column;gap:24px;max-width:360px">
  <div class="eq-token-meter" data-state="warn">
    <div class="eq-token-meter-head" aria-hidden="true">
      <span class="eq-token-meter-label">Context window</span>
      <span class="eq-token-meter-figure">109.5k of 128k</span>
    </div>
    <div class="eq-token-meter-bar" role="meter" aria-label="Context window" aria-valuemin="0" aria-valuemax="128000" aria-valuenow="109500" aria-valuetext="109.5k of 128k tokens used. Getting full.">
      <span class="eq-token-meter-segment" style="--_share:6.406249999999999%"></span>
      <span class="eq-token-meter-segment" style="--_share:55.859375%"></span>
      <span class="eq-token-meter-segment" style="--_share:23.28125%"></span>
    </div>
    <ul class="eq-token-meter-legend">
      <li class="eq-token-meter-item">
        <span class="eq-token-meter-swatch" aria-hidden="true"></span>
        <span class="eq-token-meter-name">System</span>
        <span class="eq-token-meter-amount">8.2k</span>
      </li>
      <li class="eq-token-meter-item">
        <span class="eq-token-meter-swatch" aria-hidden="true"></span>
        <span class="eq-token-meter-name">Messages</span>
        <span class="eq-token-meter-amount">71.5k</span>
      </li>
      <li class="eq-token-meter-item">
        <span class="eq-token-meter-swatch" aria-hidden="true"></span>
        <span class="eq-token-meter-name">Files</span>
        <span class="eq-token-meter-amount">29.8k</span>
      </li>
      <li class="eq-token-meter-item" data-free="">
        <span class="eq-token-meter-swatch" aria-hidden="true"></span>
        <span class="eq-token-meter-name">Free</span>
        <span class="eq-token-meter-amount">18.5k</span>
      </li>
    </ul>
    <p class="eq-token-meter-state">Getting full</p>
  </div>
  <div class="eq-token-meter" data-state="over">
    <div class="eq-token-meter-head" aria-hidden="true">
      <span class="eq-token-meter-label">Context window</span>
      <span class="eq-token-meter-figure">144k of 128k</span>
    </div>
    <div class="eq-token-meter-bar" role="meter" aria-label="Context window" aria-valuemin="0" aria-valuemax="128000" aria-valuenow="128000" aria-valuetext="144k of 128k tokens used. Over the limit.">
      <span class="eq-token-meter-segment" style="--_share:5.694444444444445%"></span>
      <span class="eq-token-meter-segment" style="--_share:65.34722222222223%"></span>
      <span class="eq-token-meter-segment" style="--_share:28.958333333333336%"></span>
    </div>
    <ul class="eq-token-meter-legend">
      <li class="eq-token-meter-item">
        <span class="eq-token-meter-swatch" aria-hidden="true"></span>
        <span class="eq-token-meter-name">System</span>
        <span class="eq-token-meter-amount">8.2k</span>
      </li>
      <li class="eq-token-meter-item">
        <span class="eq-token-meter-swatch" aria-hidden="true"></span>
        <span class="eq-token-meter-name">Messages</span>
        <span class="eq-token-meter-amount">94.1k</span>
      </li>
      <li class="eq-token-meter-item">
        <span class="eq-token-meter-swatch" aria-hidden="true"></span>
        <span class="eq-token-meter-name">Files</span>
        <span class="eq-token-meter-amount">41.7k</span>
      </li>
    </ul>
    <p class="eq-token-meter-state">Over the limit</p>
  </div>
</div>

Compact, in a header

Compact drops the legend and the state line; the full reading is still in the meter's value text.

Rollback plan for build 412
Code
import { TokenMeter } from "@/components/equip/token-meter";

export default function Example() {
  return (
    <div
      style={{
        display: "flex",
        flexWrap: "wrap",
        alignItems: "center",
        justifyContent: "space-between",
        gap: 16,
        maxWidth: 420,
        paddingBlock: 8,
        borderBottom: "1px solid var(--eq-line)",
      }}
    >
      <strong style={{ fontSize: "var(--eq-text-s)" }}>Rollback plan for build 412</strong>
      <TokenMeter
        compact
        style={{ flex: "none", width: 140 }}
        label="Context window"
        limit={200_000}
        segments={[
          { label: "System", value: 12_400 },
          { label: "Messages", value: 151_000 },
        ]}
      />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:16px;max-width:420px;padding-block:8px;border-bottom:1px solid var(--eq-line)">
  <strong style="font-size:var(--eq-text-s)">Rollback plan for build 412</strong>
  <div style="flex:none;width:140px" class="eq-token-meter" data-state="warn" data-compact="true">
    <div class="eq-token-meter-bar" role="meter" aria-label="Context window" aria-valuemin="0" aria-valuemax="200000" aria-valuenow="163400" aria-valuetext="163.4k of 200k tokens used. Getting full.">
      <span class="eq-token-meter-segment" style="--_share:6.2%"></span>
      <span class="eq-token-meter-segment" style="--_share:75.5%"></span>
    </div>
    <span class="eq-token-meter-figure" aria-hidden="true">163.4k of 200k</span>
  </div>
</div>

A monthly budget in percent

  • Support bot37%
  • Code review28%
  • Search12%
  • Free22%

Getting full

Code
import { TokenMeter } from "@/components/equip/token-meter";

export default function Example() {
  return (
    <TokenMeter
      style={{ maxWidth: 360 }}
      label="Team usage this month"
      format="percent"
      warnAt={0.75}
      limit={5_000_000}
      segments={[
        { label: "Support bot", value: 1_850_000 },
        { label: "Code review", value: 1_420_000 },
        { label: "Search", value: 610_000 },
      ]}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<div style="max-width:360px" class="eq-token-meter" data-state="warn">
  <div class="eq-token-meter-head" aria-hidden="true">
    <span class="eq-token-meter-label">Team usage this month</span>
    <span class="eq-token-meter-figure">78%</span>
  </div>
  <div class="eq-token-meter-bar" role="meter" aria-label="Team usage this month" aria-valuemin="0" aria-valuemax="5000000" aria-valuenow="3880000" aria-valuetext="78% used. Getting full.">
    <span class="eq-token-meter-segment" style="--_share:37%"></span>
    <span class="eq-token-meter-segment" style="--_share:28.4%"></span>
    <span class="eq-token-meter-segment" style="--_share:12.2%"></span>
  </div>
  <ul class="eq-token-meter-legend">
    <li class="eq-token-meter-item">
      <span class="eq-token-meter-swatch" aria-hidden="true"></span>
      <span class="eq-token-meter-name">Support bot</span>
      <span class="eq-token-meter-amount">37%</span>
    </li>
    <li class="eq-token-meter-item">
      <span class="eq-token-meter-swatch" aria-hidden="true"></span>
      <span class="eq-token-meter-name">Code review</span>
      <span class="eq-token-meter-amount">28%</span>
    </li>
    <li class="eq-token-meter-item">
      <span class="eq-token-meter-swatch" aria-hidden="true"></span>
      <span class="eq-token-meter-name">Search</span>
      <span class="eq-token-meter-amount">12%</span>
    </li>
    <li class="eq-token-meter-item" data-free="">
      <span class="eq-token-meter-swatch" aria-hidden="true"></span>
      <span class="eq-token-meter-name">Free</span>
      <span class="eq-token-meter-amount">22%</span>
    </li>
  </ul>
  <p class="eq-token-meter-state">Getting full</p>
</div>

Props

PropTypeDefaultWhat it does
labelstringnoneWhat is measured, e.g. "Context window". Also the meter's accessible name.
segments{ label: string; value: number }[]noneWhat the spend is made of, in tokens, in the order they fill the bar.
limitnumbernoneSize of the window or budget, in tokens.
format"tokens" | "percent""tokens"Figures as counts like "142k of 200k" or as a share like "71%".
warnAtnumber0.8Share of the limit from which the state reads Getting full.
compactbooleanfalseA single thin bar with a number, for a header.
...restComponentProps<"div">noneEvery native div attribute.

Anatomy

root.eq-token-meter
head.eq-token-meter-head
label.eq-token-meter-label
figure.eq-token-meter-figureUsed and limit, or the share.
bar.eq-token-meter-barThe element with role meter.
segment.eq-token-meter-segmentTones step down from the ink in order.
legend.eq-token-meter-legend
legend item.eq-token-meter-itemThe free space is the last item, marked data-free.
swatch.eq-token-meter-swatch
name.eq-token-meter-name
amount.eq-token-meter-amount
state.eq-token-meter-statePlenty of room, Getting full or Over the limit.

Notes

  • The bar is a meter whose value text reads, for example, "142k of 200k tokens used. Plenty of room."
  • Over the limit, aria-valuenow stops at the limit while the value text carries the true figure, and the bar is scaled to the total so the mix stays visible.
  • Numbers are formatted by the part, not by Intl, so the server and the browser always print the same "12.4k".
  • The state is written in words; the coloured dot beside it is never the only signal.
  • Renders on the server with no client JavaScript.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/token-meter.tsx113 lines
import type { ComponentProps, CSSProperties } from "react";
import "./token-meter.css";

export type TokenMeterProps = ComponentProps<"div"> & {
  /** What is being measured, e.g. "Context window". Also the meter's accessible name. */
  label: string;
  /** What the spend is made of, in tokens, in the order they fill the bar. */
  segments: { label: string; value: number }[];
  /** The size of the window or budget, in tokens. */
  limit: number;
  /** Show figures as token counts ("142k of 200k") or as a share ("71%"). */
  format?: "tokens" | "percent";
  /** Share of the limit from which the state reads "Getting full". */
  warnAt?: number;
  /** A single thin bar with a number, for a header or toolbar. */
  compact?: boolean;
};

/** "940", "12.4k", "200k", "1.2M". Hand-rolled so server and client print the same thing. */
function short(n: number): string {
  const v = Math.max(0, n);
  const trim = (x: number) => (Math.round(x * 10) / 10).toFixed(1).replace(/\.0$/, "");
  if (v < 1000) return String(Math.round(v));
  if (v < 999_950) return `${trim(v / 1000)}k`;
  return `${trim(v / 1_000_000)}M`;
}

export function TokenMeter({
  label,
  segments,
  limit,
  format = "tokens",
  warnAt = 0.8,
  compact = false,
  className,
  ...rest
}: TokenMeterProps) {
  const used = segments.reduce((sum, s) => sum + Math.max(0, s.value), 0);
  const safeLimit = Math.max(0, limit);
  const share = safeLimit > 0 ? used / safeLimit : used > 0 ? Infinity : 0;
  const state = share > 1 ? "over" : share >= warnAt ? "warn" : "ok";
  const stateText = { ok: "Plenty of room", warn: "Getting full", over: "Over the limit" }[state];
  const percent = Number.isFinite(share) ? Math.round(share * 100) : 100;
  const figure = format === "percent" ? `${percent}%` : `${short(used)} of ${short(safeLimit)}`;
  const spoken = format === "percent" ? `${percent}% used` : `${short(used)} of ${short(safeLimit)} tokens used`;
  // Over the limit the bar is scaled to the total, so it fills and still shows the mix.
  const scale = Math.max(safeLimit, used, 1);
  const free = Math.max(0, safeLimit - used);

  return (
    <div
      {...rest}
      className={["eq-token-meter", className].filter(Boolean).join(" ")}
      data-state={state}
      data-compact={compact || undefined}
    >
      {!compact && (
        <div className="eq-token-meter-head" aria-hidden="true">
          <span className="eq-token-meter-label">{label}</span>
          <span className="eq-token-meter-figure">{figure}</span>
        </div>
      )}
      <div
        className="eq-token-meter-bar"
        role="meter"
        aria-label={label}
        aria-valuemin={0}
        aria-valuemax={safeLimit}
        aria-valuenow={Math.min(used, safeLimit)}
        aria-valuetext={`${spoken}. ${stateText}.`}
      >
        {segments.map((segment, i) => (
          <span
            key={`${i}-${segment.label}`}
            className="eq-token-meter-segment"
            style={{ "--_share": `${(Math.max(0, segment.value) / scale) * 100}%` } as CSSProperties}
          />
        ))}
      </div>
      {compact ? (
        <span className="eq-token-meter-figure" aria-hidden="true">
          {figure}
        </span>
      ) : (
        <>
          <ul className="eq-token-meter-legend">
            {segments.map((segment, i) => (
              <li key={`${i}-${segment.label}`} className="eq-token-meter-item">
                <span className="eq-token-meter-swatch" aria-hidden="true" />
                <span className="eq-token-meter-name">{segment.label}</span>
                <span className="eq-token-meter-amount">
                  {format === "percent" && safeLimit > 0
                    ? `${Math.round((Math.max(0, segment.value) / safeLimit) * 100)}%`
                    : short(segment.value)}
                </span>
              </li>
            ))}
            {free > 0 && (
              <li className="eq-token-meter-item" data-free="">
                <span className="eq-token-meter-swatch" aria-hidden="true" />
                <span className="eq-token-meter-name">Free</span>
                <span className="eq-token-meter-amount">
                  {format === "percent" ? `${Math.round((free / safeLimit) * 100)}%` : short(free)}
                </span>
              </li>
            )}
          </ul>
          <p className="eq-token-meter-state">{stateText}</p>
        </>
      )}
    </div>
  );
}
components/equip/token-meter.css163 lines
@layer equip {
  .eq-token-meter {
    display: flex;
    flex-direction: column;
    gap: var(--eq-space-2);
    min-width: 0;
    color: var(--eq-ink);
    font: var(--eq-weight) var(--eq-text-s) / 1.3 var(--eq-font);
  }

  .eq-token-meter[data-compact] {
    flex-direction: row;
    align-items: center;
  }

  .eq-token-meter-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--eq-space-3);
  }

  .eq-token-meter-label {
    font-weight: var(--eq-weight-strong);
  }

  .eq-token-meter-figure {
    color: var(--eq-ink-2);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  .eq-token-meter[data-compact] .eq-token-meter-figure {
    flex: none;
    font-size: var(--eq-text-xs);
  }

  /* Tones step down from the ink. They are translucent, so a 1px gap keeps neighbours apart. */
  .eq-token-meter-segment,
  .eq-token-meter-swatch {
    --_tone: var(--eq-ink);
  }

  .eq-token-meter-segment:nth-child(2),
  .eq-token-meter-item:nth-child(2) .eq-token-meter-swatch {
    --_tone: color-mix(in srgb, var(--eq-ink) 60%, transparent);
  }

  .eq-token-meter-segment:nth-child(3),
  .eq-token-meter-item:nth-child(3) .eq-token-meter-swatch {
    --_tone: color-mix(in srgb, var(--eq-ink) 36%, transparent);
  }

  .eq-token-meter-segment:nth-child(n + 4),
  .eq-token-meter-item:nth-child(n + 4) .eq-token-meter-swatch {
    --_tone: color-mix(in srgb, var(--eq-ink) 20%, transparent);
  }

  .eq-token-meter-bar {
    display: flex;
    gap: 1px;
    height: 8px;
    overflow: hidden;
    border-radius: var(--eq-radius-s);
    background: var(--eq-tint);
  }

  .eq-token-meter[data-compact] .eq-token-meter-bar {
    flex: 1;
    min-width: var(--eq-space-6);
    height: 4px;
  }

  /* Compact has no state line, so the number carries the warning. */
  .eq-token-meter[data-compact][data-state="warn"] .eq-token-meter-figure {
    color: var(--eq-warn);
  }

  .eq-token-meter[data-compact][data-state="over"] .eq-token-meter-figure {
    color: var(--eq-danger);
  }

  .eq-token-meter[data-state="over"] .eq-token-meter-bar {
    box-shadow: inset 0 0 0 1px var(--eq-danger);
  }

  .eq-token-meter-segment {
    flex: 0 0 var(--_share);
    min-width: 0;
    background: var(--_tone);
    transition:
      flex-basis var(--eq-dur-move) var(--eq-spring),
      background-color var(--eq-dur) var(--eq-ease);
  }

  .eq-token-meter-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--eq-space-1) var(--eq-space-3);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--eq-text-xs);
  }

  .eq-token-meter-item {
    display: inline-flex;
    align-items: center;
    gap: var(--eq-space-1);
    white-space: nowrap;
  }

  .eq-token-meter-swatch {
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: 2px;
    background: var(--_tone);
  }

  .eq-token-meter-item[data-free] .eq-token-meter-swatch {
    background: var(--eq-tint);
    box-shadow: inset 0 0 0 1px var(--eq-line-strong);
  }

  .eq-token-meter-name {
    color: var(--eq-ink-2);
  }

  .eq-token-meter-amount {
    font-variant-numeric: tabular-nums;
    font-weight: var(--eq-weight-strong);
  }

  .eq-token-meter-state {
    display: flex;
    align-items: center;
    gap: var(--eq-space-1);
    margin: 0;
    color: var(--eq-ink-2);
    font-size: var(--eq-text-xs);
  }

  .eq-token-meter-state::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: var(--eq-radius-full);
    background: var(--eq-ok);
  }

  .eq-token-meter[data-state="warn"] .eq-token-meter-state::before {
    background: var(--eq-warn);
  }

  .eq-token-meter[data-state="over"] .eq-token-meter-state {
    color: var(--eq-danger);
  }

  .eq-token-meter[data-state="over"] .eq-token-meter-state::before {
    background: var(--eq-danger);
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index