INP Optimization
A compact comparison panel for optimization techniques that lower Interaction to Next Paint. Pass a list of techniques ({ id, label, beforeMs, afterMs }); the widget surfaces a tablist, a side-by-side before / after comparison with proportional bars scaled against the larger of the two totals, and a summary line that quantifies the savings.
Preview
Pick an optimization
Each technique brings the same interaction under the web.dev INP good band.
Break the click handler with scheduler.yield() so the browser can paint between work chunks.
Before420ms
After180ms
Interaction latency dropped by 240ms (57%).After: 180ms · target ≤ 0.2s.
Customize
Yield on input
420ms
180ms
Defer to rAF
380ms
170ms
Isolate long task
620ms
160ms
Options
Installation
npx shadcn@latest add https://craftbits.dev/r/inp-optimization.jsonUsage
import { InpOptimization } from "@craft-bits/core";
<InpOptimization
title="Pick an optimization"
techniques={[
{ id: "yield-on-input", label: "Yield on input", beforeMs: 420, afterMs: 180 },
{ id: "raf-paint", label: "Defer to rAF", beforeMs: 380, afterMs: 170 },
{ id: "isolate-loaf", label: "Isolate long task", beforeMs: 620, afterMs: 160 },
]}
/>Controlled mode — drive the active technique from outside so the choice can feed an external lesson stepper:
const [active, setActive] = useState("yield-on-input");
<InpOptimization
techniques={TECHNIQUES}
activeTechnique={active}
onActiveTechniqueChange={setActive}
/>Anatomy
- Header. Optional
titleanddescription. - Tablist. One pill per technique. The active pill picks up an inset accent shadow and
data-cb-active. - Before / After columns. Two side-by-side cards with verdict-toned totals and proportional bars on a shared scale, so the saving is visible at a glance.
- Summary. Absolute savings (ms), relative reduction (%), after-total, and the
target ≤line. Verdict tone tracks the after-total.
Understanding the component
- Bars share a scale. Both columns scale their bars against
max(beforeMs, afterMs)so the after-bar reads as a fraction of the before-bar, not as a fraction of itself. - Verdict bucket. Each total is graded against
goodMs(default 200) andpoorMs(default 500).goodpaints--cb-success,needs-improvementpaints--cb-warning,poorpaints--cb-error. - Controlled + uncontrolled. Provide
activeTechniquewithonActiveTechniqueChange, ordefaultActiveTechniquefor the uncontrolled case. Omit both and the first technique is active. - Motion. Bar fills animate with the
snapspring on every technique change and short-circuit underprefers-reduced-motion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
techniques | InpOptimizationTechnique[] | required | The techniques to compare. |
activeTechnique | string | — | Controlled active-technique id. |
defaultActiveTechnique | string | first id | Initial active id in uncontrolled mode. |
onActiveTechniqueChange | (next) => void | — | Fires on tab change. |
title | ReactNode | — | Optional heading above the panel. |
description | ReactNode | — | Optional sub-headline under the title. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
goodMs | number | 200 | Threshold for the good verdict. |
poorMs | number | 500 | Threshold for the poor verdict. |
beforeLabel | ReactNode | 'Before' | Label on the "before" column. |
afterLabel | ReactNode | 'After' | Label on the "after" column. |
hideSummary | boolean | false | Hide the savings summary line. |
tablistAriaLabel | string | 'INP optimization' | ARIA label for the tablist. |
className | string | — | Merged onto the root via cn(). |
InpOptimizationTechnique
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. Surfaces via data-cb-tech. |
label | ReactNode | Display label on the tab. |
beforeMs | number | Total interaction latency before the technique, in ms. |
afterMs | number | Total interaction latency after the technique, in ms. |
description | ReactNode | Optional one-line description shown under the tablist. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="inp-optimization"anddata-cb-verdict="good" | "needs-improvement" | "poor"tracking the after-total. - The tablist uses native
role="tablist"+role="tab"; the active tab carriesaria-selected="true". - Each bar is a
role="progressbar"witharia-valuenow/aria-valuemin/aria-valuemaxand a descriptivearia-label. - The summary row is
aria-live="polite"so screen readers announce the savings on tab change. - Bar fills short-circuit under
prefers-reduced-motion.