# Custom Inspector Tabs
> Source: `src/content/docs/actors/inspector-tabs.mdx`
> Canonical URL: https://rivet.dev/docs/actors/inspector-tabs
> Description: Ship your own UI tabs alongside a Rivet Actor — embedded directly in the dashboard inspector.
---
Custom inspector tabs let you embed your own UI directly in the Rivet
dashboard, next to the built-in tabs. Declare a tab on your actor, point
it at a folder of static files, and the dashboard picks it up
automatically.
Common uses:
- Domain-specific debugging panels (queue depth, connection maps, log
filters).
- Operational tools (admin buttons, drain controls, snapshot uploaders).
- Any author-defined view that ships with your actor.
A runnable example lives at
[`examples/inspector-tabs`](https://github.com/rivet-dev/rivet/tree/main/examples/inspector-tabs).
## Quickstart
Declare a tab on your actor:
Drop an `index.html` in the `source` directory:
```html
Counter: …
```
Open the dashboard and the "Counter" tab appears alongside the built-ins.
## Configuration
Each entry in `inspector.tabs[]` is either a **custom tab** or a
**hide modifier** for a built-in.
### Custom tab
```ts @nocheck
{
id: string, // URL-safe id: /^[A-Za-z0-9_-]+$/
label: string, // Shown in the tab strip
source: string, // Directory of static assets
icon?: string, // Optional icon id
}
```
- **`id`** — used as the URL segment and tab-strip key. Cannot collide
with a built-in id (`workflow`, `database`, `state`, `queue`,
`connections`, `console`).
- **`source`** — directory of static files. The bytes you put there are
the bytes the browser sees. Point it at a Vite/webpack `dist/` and
any framework works (React, Vue, Svelte, vanilla — all fine).
- **`icon`** — one of `workflow`, `database`, `state`, `queue`, `plug`,
`terminal`, `tag`, `logs`. Anything else falls back to a neutral icon.
### Hide a built-in tab
```ts @nocheck
{
id: "workflow" | "database" | "state" | "queue" | "connections" | "console",
hidden: true,
}
```
Use this to clean up the strip when the actor doesn't use a given
subsystem — e.g. a counter actor with no queues:
```ts
inspector: { tabs: [{ id: "queue", hidden: true }] }
```
Misconfigurations (missing directory, duplicate id, invalid characters,
empty label) throw at registry construction, so problems show up
immediately.
## Talking to the dashboard
The tab loads in an iframe and communicates with the dashboard via
`postMessage`. The contract is small.
### From the dashboard
The dashboard sends an `init` message on load and again whenever the
inspector token rotates. Always overwrite the cached token when it
arrives.
```ts @nocheck
{
type: "init",
v: 1,
actorId: string,
authToken: string, // Per-actor inspector bearer token
theme?: "light" | "dark",
activeTab?: string, // For multi-view tabs
}
```
Multi-view tabs can read the optional `activeTab` field on `init` to
seed their initial sub-view. The dashboard does not send a separate
message when the user switches custom tabs — it navigates the iframe
`src` instead, so the tab reloads and receives a fresh `init`.
### From the tab
Send `ready` once your message listener is registered:
```ts @nocheck
{ type: "ready", v: 1 }
```
If a fetch returns 401, the token has rotated. Ask the dashboard for a
fresh one and wait for the next `init` — don't retry with the stale
token:
```ts @nocheck
{ type: "token-refresh-needed", v: 1 }
```
### Security check
Always reject messages whose `event.origin` doesn't match the
`?shellOrigin=` URL parameter. Without this check, any page that frames
your tab could forge an `init` and feed you a fake token.
### TypeScript types
If you build the tab with TypeScript, the message and response types
are exported as types-only:
## Reading state and calling actions
The tab can hit any inspector endpoint with the supplied bearer token.
Use relative paths so the tab doesn't need to know the engine origin or
actor id:
```js
fetch("../../state", { headers: { Authorization: `Bearer ${token}` } });
fetch("../../action/increment", { method: "POST", headers: { ... }, body: ... });
fetch("../../rpcs", { headers: { Authorization: `Bearer ${token}` } });
fetch("../../connections", { headers: { Authorization: `Bearer ${token}` } });
fetch("../../queue", { headers: { Authorization: `Bearer ${token}` } });
```
The action body shape is `{ args: [...] }` — the array is passed as
positional arguments to the action.
Full endpoint reference:
[Debugging → Inspector Endpoints](/docs/actors/debugging#inspector-endpoints).
For high-frequency UIs, prefer the inspector WebSocket
(`/inspector/connect`) over polling.
## Styling
A shared stylesheet matching the dashboard's design tokens is served at
`../../tab.css`:
```html
```
It defines `--rivet-*` tokens for colors, spacing, radius, and
typography, plus sensible defaults so a bare tab looks at home without
custom CSS:
```css
.my-card {
background: var(--rivet-card);
color: var(--rivet-foreground);
border: 1px solid var(--rivet-border);
border-radius: var(--rivet-radius-md);
padding: var(--rivet-space-4);
}
```
Toggle dark mode by adding the `dark` class to `` — the dashboard
sends the active theme in the `init` message.
Color tokens come in both pre-wrapped (`--rivet-card`) and raw HSL
(`--rivet-card-raw`) forms, so you can compose with alpha:
```css
.overlay { background: hsl(var(--rivet-background-raw) / 0.6); }
```
You're free to skip the stylesheet entirely and bring your own.
## Security
The tab runs in an iframe at the engine origin and can call any
inspector endpoint with the supplied token. Treat the bundle like any
author code that ships with your actor:
- **Don't inline secrets.** The bundle is fetchable by anyone who can
reach the actor.
- **Always validate `event.origin`.** Reject inbound messages from
anywhere other than the dashboard origin.
- **Never retry silently on 401.** Post `token-refresh-needed` and wait
for a fresh `init`.
## See also
- [Debugging](/docs/actors/debugging) — full inspector HTTP API
- [Actions](/docs/actors/actions) — actions your tab can invoke
- [State](/docs/actors/state) — state your tab can read
- [`examples/inspector-tabs`](https://github.com/rivet-dev/rivet/tree/main/examples/inspector-tabs) — runnable example
_Source doc path: /docs/actors/inspector-tabs_