Skip to content
v0.12.1 · MIT · 0 runtime deps

A bottom bar that hosts your tools.

An extensible, low-overhead in-app developer toolbar for React. The package is chrome plus hosting — metrics, flags, overlays and diagnostics are opt-in extensions, each on its own subpath with its own bundle.

React 18 / 19peerLight DOMtailwind & css-in-js workSSR-safeclient-only bar⌘⇧.show / hide on Mac
the bar
env stagingcmds aggregatedUI Facelift 2026flags6theme 14mem23 MBdelayjank0.0%net0hydrNAtailwind
⌘Kuser internal

At real size, with the run of chips clipping into ⋮ exactly as the shell collapses overflow on a narrow viewport. The shell sorts and collapses the items you give it, hosts one panel at a time, remembers preferences and isolates failures. A compact or panel slot that throws becomes a retry chip; an overlay that throws is reported without one.

zero runtime deps
React and react-dom are peers; package runtime adds nothing. ext/a11y uses optional axe-core.
restyleable, no !important
Core CSS lives in @layer dev-toolbar, so unlayered author CSS wins at any specificity.
failure-isolated
One extension throwing never takes the bar down — and the rest keeps working.
light DOM
No shadow root, so Tailwind, CSS-in-JS and your design system work inside extensions.
logical properties
dir="rtl" mirrors the bar, the ⋮ popup and every first-party extension.
ssr-safe
Children server-render untouched; the bar is client-only, so nothing can mismatch.

install

$ npm install @nejcm/dev-toolbar# react & react-dom 18 or 19 are required peers# axe-core (ext/a11y) and @testing-library/react (/testing)# are optional peers — install them only if you use those

nine first-party extensions

Each one an opt-in subpath with its own bundle. Import none and the bar hosts only your tools.

/kit
types, helpers, data-dtb-kind CSS, native-looking controls
/runtime
event bus, ring buffers, throttled store, redact()
/testing
renderWithToolbar, fake layout, mock bus — needs the optional @testing-library/react peer
/styles.css
the shell stylesheet, if you would rather import it

write your own

// An extension is a plain object.// No registry, no class, no plugin API.const build: DevToolbarExtension = {  id: "build-info",  label: "Build",  align: "end",  compact: ({ openPanel }) => (    <button data-dtb-part="trigger" onClick={openPanel}>      {import.meta.env.VITE_COMMIT?.slice(0, 7) ?? "dev"}    </button>  ),  panel: () => <BuildDetails />,};
commands

Add them and the extension appears in the ⌘K palette. Contract v2 commands may declare an input schema and resolve a result.

diagnostics()

What you return lands in somebody's bug report — and they read the exact text first.

start(api)

Background work with an AbortSignal that fires on teardown.

two rules that save an afternoon

Build the object once, at module scope. And treat hidden as does not exist here, not unpainted.

documentation

contributing

the one command that matters
$ bun run verify

format:check → typecheck → lint → knip → build → check:package → test. If it passes locally it passes in CI.

try a change for real
$ bun run playground

A Vite app on :5273 consuming the built dist through file:../.. exactly as a published consumer does.

house rules

Bun, not npm. Zero runtime dependencies is a rule. Core never imports runtime/ or ext/. Conventional Commits, enforced — the PR title is the message that lands.

Put your own devtools where you already look.

$ npm install @nejcm/dev-toolbarRead the README

Released under the MIT License.