Skip to main content
Switchbord’s app shell is built around a collapsible sidebar, an auto-derived breadcrumb in the top bar, and a small set of composable page primitives (PageHeader, FilterTabs, Tabs, Card, plain sections). This page documents what’s where, and — for contributors — when to use each primitive so new screens look and feel like the rest of the product. The left sidebar uses collapsible groups. Each group is a parent route; its children are a flat list of first-class destinations underneath.

Inbox

Flat — no sub-group. Land directly on the operator inbox.

Contacts

Flat. Filters and segments live inside the page, not the sidebar.

Agents

Collapsible group. Children: Overview, Templates, and per-agent detail routes.

Campaigns

Collapsible group. Children: All campaigns, Audiences, Schedules.

Journeys

Collapsible group. Children: All journeys, Templates, Runs.

Settings

Flat; internal tabs handle the many sub-pages.
Groups persist their open/closed state per user in localStorage so the shell doesn’t re-collapse on every navigation.
If you’re adding a new top-level destination, prefer a flat entry first. Only promote to a collapsible group once you have three or more sibling routes that benefit from being visible at once.

Sub-routes under a parent record

Detail pages for Agents, Campaigns, and Journeys use a consistent URL shape:
These sub-routes are rendered as FilterTabs at the top of the record page, immediately below the PageHeader. Each tab is a real route — deep-linkable, shareable, and preserved across reloads. Tab switches use Next.js shallow navigation so scroll position and transient state are not blown away.
Sub-route tabs are not the same primitive as in-page Tabs. See Filter tabs vs content tabs below.
The top bar renders a breadcrumb trail that is auto-derived from the current route by default. The system walks the pathname segments, looks each up in a small registry (/apps/app/lib/breadcrumbs.ts) for a human label, and renders clickable crumbs for every segment except the last.

Auto mode (the default)

Every page gets auto-derived crumbs without doing anything:
For /agents/abc123/runs this renders: Agents › Acme support bot › Runs. The middle label is resolved by the registry’s [id] loader (it fetches the agent’s display name from cache). Use auto mode when:
  • The route segments map cleanly to human labels (the common case).
  • You’re inside a parent/child record hierarchy already wired into the registry.

Explicit mode

Pass breadcrumbs as an array when the auto-derived trail would be wrong or missing context:
Use explicit mode when:
  • The URL doesn’t reflect the user’s mental location (e.g. modal routes, wizards).
  • You need to surface a label that isn’t in the registry yet and you don’t want to add it.
  • You’re rendering a one-off page (migrations, admin tools) where registry maintenance isn’t worth it.
Don’t mix both. If you pass breadcrumbs, the auto resolver is skipped entirely — partial overrides are not supported on purpose, to keep the trail predictable.

Filter tabs vs content tabs

Switchbord ships two tab primitives that look similar but serve different roles.

FilterTabs — segmented control

A compact, segmented-button row. Picks which slice of the same list you’re viewing. No content panels of its own — it just controls the query/filter state below. Use FilterTabs when:
  • Switching between views of the same underlying dataset (e.g. All / Draft / Pending / Approved on templates).
  • You want a segmented-control look that doesn’t imply separate “pages”.
  • Sub-route navigation for a detail record (config | tools | mcp | runs).

Tabs — content regions

The full Radix-backed Tabs primitive with TabsList, TabsTrigger, and TabsContent. Each tab owns a distinct content region with its own header, its own data, and potentially its own mutations. Use Tabs when:
  • The tabs render structurally different UIs (e.g. Overview vs JSON vs Logs).
  • Each tab has substantial content — tables, forms, charts.
  • You’d otherwise have to split into sibling routes but the screens are too tightly coupled to warrant URL-level navigation.
Rule of thumb: if switching the tab should change the URL, use FilterTabs with sub-routes. If switching the tab only changes what’s inside this card / section, use Tabs.

Cards vs sections — page composition

The most common mistake in new screens is wrapping everything in a Card. Our page grammar is:
  1. PageHeader at the top — title, description, breadcrumbs, primary action.
  2. Plain sections underneath — an <h2> (or a small section header component) followed by content directly on the page background.
  3. Card only for side panels, modular pickers, or things that genuinely need a bordered container.

Prefer plain sections

This reads as one page with two concerns, which is usually what you mean. Stacking two full Cards produces heavy, boxy pages that don’t scale past three sections.

Reach for Card when

  • Side panels — a right-rail summary next to the main content.
  • Modular pickers — model selector, template picker, audience filter box. Self-contained widgets that can live anywhere on a page.
  • Empty states & onboarding prompts — a single, centred affordance on an otherwise empty page.
  • Dashboard tiles — metric cards on overview pages.
If you find yourself nesting a Card inside a Card, stop. That’s the signal that the outer container should be a plain section, or the inner one should be a plain <div>.

Quick checklist for new pages

Before you open a PR for a new screen, confirm:
  • PageHeader is the first element; breadcrumbs is either omitted (auto) or an explicit array — never both.
  • Sub-routes under a record use FilterTabs wired to real URLs, not in-page Tabs.
  • The sidebar entry is flat unless there are 3+ sibling routes, in which case it’s a collapsible group with a sensible icon.
  • Top-level structure is PageHeader + sections. Card appears only for side panels, pickers, empty states, or dashboard tiles.
  • Any new [id] segment has a label loader registered in /apps/app/lib/breadcrumbs.ts (or explicit crumbs are passed at every call site).

Shipped in this project

The navigation + UI polish project (BORD-329 → BORD-333) delivered:
  • BORD-329 — collapsible sidebar groups for Agents / Campaigns / Journeys ✅
  • BORD-330 — per-record sub-route tabs (config | tools | mcp | runs and friends) ✅
  • BORD-331 — auto-derived breadcrumbs with a small registry + explicit override escape hatch ✅
  • BORD-332 — page-grammar refactor: PageHeader + sections, Card demoted to side-panel duty ✅
  • BORD-333 — this doc + a design-token cleanup on the /web landing CTA ✅