Why I built dev-toolbar
I wanted a dev toolbar after reading about the one Linear uses inside its app. In their post about the design refresh, they describe toggling feature flags from the toolbar to compare the refreshed interface with the previous version. They also describe a color tool inside it for adjusting design tokens. The useful part for me was the location: change something, then see the result in the app you’re already working on.
Their StyleX migration post has another example. Their performance toolbar shows a styled-components CSS rule count for the current page. That’s a tool specific to a piece of work, useful while that work is happening. It made me want somewhere to put tools like that in my own apps.
nejcm/dev-toolbarReact developer toolbar with a small shell and opt-in tools.TypeScript(opens in new tab)I wanted to build a generic version that other developers could drop into their apps too. That became dev-toolbar, a React toolbar that hosts tools you choose. The part I wanted to share was the bar, the panels, and the wiring around them. The tools themselves need to depend on the application.
A feature flag control is a good example of that boundary. The toolbar can display a flag and let you override it, but your app knows where flags come from and how an override changes its behavior. I don’t want adding a toolbar to mean adopting another flag service or replacing a tool you already use.
The package is a shell. It handles ordering, overflow on narrow viewports, one open panel at a time, and saved preferences. Metrics, environment information, flags, and the other first-party tools are opt-in extensions. Each has its own import path and bundle. Import none of them and you can fill the bar entirely with your own tools.
Even the command menu is an extension. Core collects commands; the menu provides a way to find and run them. I like that separation because it leaves room for an app that already has a command palette. You shouldn’t need two just to use the toolbar.
The docs site shows a starting setup with environment information and metrics. You create the extensions once, outside render, and pass the array to DevToolbar. Adding another tool means importing its factory and adding the result to that array. There isn’t a preset collection you have to accept first.
Bring your own tools
A custom extension is a plain object. It needs an ID and a label; the rendering slots are optional. Here’s a small build-info tool using the same shape as the extension contract example:
import type { DevToolbarExtension } from "@nejcm/dev-toolbar";
const build: DevToolbarExtension = {
id: "build-info",
label: "Build",
compact: ({ openPanel }) => (
<button type="button" data-dtb-part="trigger" onClick={openPanel}>
{import.meta.env.VITE_COMMIT?.slice(0, 7) ?? "dev"}
</button>
),
panel: () => <p>{import.meta.env.VITE_COMMIT ?? "Local build"}</p>,
};
This example uses Vite’s environment variables. Add build to the extensions array and the button opens its panel. There’s no base class or registration service. If you only supply a panel, core can supply the labelled trigger too.
The object belongs at module scope because its identity owns its lifecycle. For background work, an extension can provide start(api), with an abort signal for teardown and an optional cleanup function. It can also contribute commands and diagnostics. A label doesn’t need all of that. A collector might.
Bringing your own tools also means keeping tools someone else already wrote. The embedding docs show a TanStack Query devtools panel hosted through the same contract. You supply the panel and its stylesheet. The toolbar supplies the trigger and panel hosting. Extensions render in the light DOM, so your existing styles can reach them; the optional kit provides controls if you want to match the first-party panels.
I kept the package at zero runtime dependencies, with React and react-dom as required peers. The accessibility extension uses axe-core as an optional peer. Those costs should follow the tools you choose.
Deployment is your choice too. enabled={false} stops the toolbar from mounting but leaves its imports in the bundle. To exclude the code, put a dynamic import behind a build-time deployment condition. The README covers both approaches, including the staging trap: a staging deployment can still be a production build, so NODE_ENV alone may exclude the toolbar exactly where you wanted it available.