Build your own devtools
hakka-browser/elements ships the six pieces the floating hakka-inspector
overlay is built from — <hakka-request-list>, <hakka-request-detail>, <hakka-waterfall>,
<hakka-filter-bar>, <hakka-stats>, <hakka-json-tree> — as standalone, framework-agnostic
custom elements. Same SolidJS source the overlay renders, same Shadow DOM styling, no panel
shell, no launcher, no command palette around it.
That means you don’t have to choose between “the whole overlay” and “roll your own network table.” Drop the one piece you need wherever you’re already building UI:
- An internal admin page — a
<hakka-request-list>+<hakka-request-detail>pair, wired to your own capture store, sitting next to your other ops tooling. No iframe, no Hakka branding chrome. - A CI/E2E report — a
<hakka-waterfall>rendering one trace group from a failed test run, embedded in the static HTML report your test runner already publishes. - An internal dashboard — a lone
<hakka-stats>panel next to your other health widgets.
Three ways to consume it
Section titled “Three ways to consume it”Straight from hakka-browser/elements, registration is always explicit — importing a subpath
never touches customElements on its own. Call register() (or registerAll() from the root
import) once, then use the tag like any other HTML element. hakka-browser/react’s wrappers
(the third tab below) handle that call for you.
No build step. Import straight from a CDN and register on demand:
<script type="module"> import { registerAll } from 'https://esm.sh/hakka-browser/elements' registerAll()</script>
<hakka-request-list compact="true"></hakka-request-list>registerAll() defines all six tags at once. Import a single subpath (e.g.
hakka-browser/elements/request-list) instead if you only want one — every subpath is its own
bundler entry point, so importing one never pulls in another element’s Solid tree.
Same registration call, through your own bundler (Vite, webpack, esbuild, …). This is the shape for the “admin page” and “E2E report” recipes below — register once at module load, then mount tags anywhere in server-rendered or static HTML:
import { register as registerList } from 'hakka-browser/elements/request-list'import { register as registerDetail } from 'hakka-browser/elements/request-detail'
registerList()registerDetail()<hakka-request-list></hakka-request-list><hakka-request-detail></hakka-request-detail>
<script type="module"> document.querySelector('hakka-request-list').addEventListener('hakka:select', (e) => { document.querySelector('hakka-request-detail').setAttribute('request-id', e.detail.id) })</script>hakka-browser/react ships thin React 19 wrappers over all six
elements — typed props and onX callback props instead of raw DOM attributes and
addEventListener:
bun add hakka-browserimport { RequestList } from 'hakka-browser/react'
export function RequestListPanel({ store }: { store?: unknown }) { return <RequestList compact store={store} onSelect={({ id }) => console.log('selected', id)} />}Registration is automatic — each component calls the underlying element’s register() for you
on mount, SSR-safe (see React → Usage for how) — no separate
registerRequestList() call needed, unlike the two tabs above.
| Component | Underlying element | Event prop |
|---|---|---|
<RequestList> |
<hakka-request-list> |
onSelect |
<RequestDetail> |
<hakka-request-detail> |
onBack |
<Waterfall> |
<hakka-waterfall> |
onSelect |
<FilterBar> |
<hakka-filter-bar> |
onFilterChange |
<Stats> |
<hakka-stats> |
— |
<JsonTree> |
<hakka-json-tree> |
— |
Every prop other than an onX callback forwards straight through as a real DOM property — no
ref escape hatch needed for store/viewModel/group/request (both typed unknown today).
See React → The interop contract for how that
forwarding works, what ref itself exposes, and why store/viewModel aren’t typed any
tighter yet.
Composition recipes
Section titled “Composition recipes”Admin page: a request list that opens a detail view
Section titled “Admin page: a request list that opens a detail view”The default, zero-config wiring: both elements default to the same module-scoped capture store, so dropping them next to each other is enough — no store to construct, no prop to pass.
<hakka-request-list id="list"></hakka-request-list><hakka-request-detail id="detail"></hakka-request-detail>
<script type="module"> import { registerRequestList } from 'hakka-browser/elements/request-list' import { registerRequestDetail } from 'hakka-browser/elements/request-detail'
registerRequestList() registerRequestDetail()
document.getElementById('list').addEventListener('hakka:select', (e) => { document.getElementById('detail').setAttribute('request-id', e.detail.id) })</script><hakka-request-detail>’s request-id attribute always resolves against the shared store
singleton — so this exact pairing only works when the list ALSO uses the shared store (the
default). If you inject a custom store into the list, set .request on the detail element
directly instead — see Components → <hakka-request-detail> resolution
order for the full precedence
rules.
E2E report: a waterfall for one failed trace
Section titled “E2E report: a waterfall for one failed trace”Group your session’s captured requests by trace with hakka-core’s own groupRequests, then hand
the one group you care about straight to <hakka-waterfall> — no store involved, it’s a pure
props-in renderer:
import { groupRequests } from 'hakka-core'import type { NetworkRequest } from 'hakka-core'import { registerWaterfall } from 'hakka-browser/elements/waterfall'
registerWaterfall()
declare function loadSessionRequests(): NetworkRequest[] // however your report loads them
const groups = groupRequests(loadSessionRequests(), 'trace')const failedTrace = groups.find((g) => g.items.some((r) => (r.status ?? 0) >= 500))
if (failedTrace) { const el = document.createElement('hakka-waterfall') as HTMLElement & { group: unknown } el.group = failedTrace document.getElementById('report-root')!.appendChild(el)}Dashboard: a stats panel next to your other widgets
Section titled “Dashboard: a stats panel next to your other widgets”<hakka-stats> has no attributes and no events — point it at a store (or leave it on the shared
default) and it renders:
<div class="widget-grid"> <hakka-stats></hakka-stats> <!-- your other dashboard widgets --></div>
<script type="module"> import { registerStats } from 'hakka-browser/elements/stats' registerStats()</script>Theming
Section titled “Theming”Every element themes the same way hakka-browser’s overlay does — --hakka-* CSS custom
properties, six curated presets, and per-element overrides, all reusing presets.ts’s exact
mechanism verbatim. Full details — the preset list, per-element override syntax, and what does
and doesn’t stay in sync with a separately installed hakka-browser — are on Components →
Theming.
If your page also runs a hakka-browser Inspector, drive both from its exported
setPreset() — hakka-browser/elements doesn’t ship a setPreset() of its own, by design
(there’s no settings UI in these six elements; that lives in the Inspector shell, out of this
entry point’s scope):
import { setPreset } from 'hakka-browser'setPreset('paper')Otherwise, reproduce the preset’s token values yourself with a per-element override — see the reference page above for the full themable token set.