Budget Widget

A compact panel that puts a set of current values against the budget each one is supposed to fit inside. Each row renders a segmented progress bar, the used / budget numbers, and a verdict tone — under / at / over. The bar keeps growing past the budget line up to a configurable cap so over-spend is visible at a glance, not silently clamped.

Preview

Performance budget

Adjust the sliders to push items over the line.

  • LCP1.82s/2.50sms
  • INP168/200ms
  • CLS0.10/0.10
  • JS bundle312/250KB

1 item over budget.

Customize
Used values
1820ms
168ms
312KB
Budgets
2500ms
250KB
Options
150%

Installation

npx shadcn@latest add https://craftbits.dev/r/budget-widget.json

Usage

import { BudgetWidget } from "@craft-bits/core";
 
<BudgetWidget
  title="Performance budget"
  items={[
    { id: "lcp", label: "LCP", used: 1820, budget: 2500, unit: "ms" },
    { id: "inp", label: "INP", used: 168, budget: 200, unit: "ms" },
    { id: "js",  label: "JS bundle", used: 312, budget: 250, unit: "KB" },
  ]}
/>

Allow a small dead zone around the budget so noisy signals don't flip into the over band on a rounding wobble:

<BudgetWidget
  items={[
    { id: "cls", label: "CLS", used: 0.1, budget: 0.1, unit: "", tolerance: 0.005 },
  ]}
/>

Anatomy

  • Header. Optional title and description. Omit both for a chromeless panel.
  • Row. Label on the left, the used / budget reading on the right, a thin progress bar underneath. The vertical divider on the bar marks the budget line — fills past it bleed into the over zone.
  • Verdict tone. under paints --cb-success, over paints --cb-error, at paints --cb-accent. Bar and numbers share the same tone.
  • Verdict summary. A one-line footer counts how many rows are over budget — hidden via hideVerdict.

Understanding the component

  1. Fill geometry. Each bar fills used / budget. The width is capped at maxFillPercent (default 150) so an item that's 5x over budget doesn't run off the layout. The budget threshold tick sits at 100 / maxFillPercent of the bar's width.
  2. Tolerance. item.tolerance declares a dead zone around the budget — a row whose absolute distance from the budget is at or below tolerance reads as at instead of under/over.
  3. Formatter. Each item can supply its own format(value) function. The default switches ms to s past 1000 and KB to MB past 1000.
  4. Motion. The bar fill animates when used or budget changes (spring snap). The animation short-circuits under prefers-reduced-motion.

Props

PropTypeDefaultDescription
itemsBudgetWidgetItem[]requiredOrdered budget rows.
titleReactNodeOptional heading above the bars.
descriptionReactNodeOptional sub-headline under the title.
maxFillPercentnumber150Bar growth cap, as a percentage of the budget.
hideVerdictbooleanfalseHide the verdict summary line.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

BudgetWidgetItem

FieldTypeDescription
idstringStable identifier.
labelReactNodeVisible row label.
usednumberCurrent spend / measurement.
budgetnumberCap to compare against (must be greater than zero).
unitstringDisplay unit suffix (e.g. ms, KB).
tolerancenumberDead zone around the budget — within it the row reads as at.
format(value) => stringCustom formatter.

Accessibility

  • The wrapper is a <section> with data-cb-edu="budget-widget". Rows form a role="list" of role="listitem" entries so screen readers announce them as a sequence.
  • Each bar is a role="progressbar" with aria-valuenow / aria-valuemin / aria-valuemax set to the percentage of budget consumed (clamped at 100). The numeric reading carries an aria-label describing the verdict so it's conveyed without depending on colour.
  • The list region uses aria-live="polite" — value updates are announced without interrupting the user.
  • Each row exposes data-cb-verdict="under" | "at" | "over" so consumers can extend tone-specific styling without monkey-patching CSS.
  • Animations are limited to the bar width and short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/sdp-web-performance/ui/BudgetWidget.tsx). The original was hard-wired to a PerfContext and a fixed LCP/INP/CLS/JS gauge set with a "Simulate careless PR" affordance that mutated a global optimisation registry. This rewrite drops the context dependency, the regression simulator, and the fixed metric list — consumers pass their own { id, label, used, budget } rows so the widget covers any budget-comparison surface (perf budgets, cloud spend, capacity plans, rate limits).