Host Chrome
Rules for the console's own first-party surfaces, so the shell and the apps it hosts read as one product.
The console is not only a frame — it has its own pages: organization settings, members, the app launcher, the admin area, the auth screens. Those pages sit directly beside embedded apps in the same navbar, so they are held to the same standard.
Everything in Foundations, Layout and Components applies to host pages unchanged. This page covers only what is specific to being the host.
What the host owns
These belong to the shell. An app must never draw them, and the host must always draw them.
| Surface | Notes |
|---|---|
| Top navbar | Fixed 40px, bg-navbar, three-column grid, not selectable |
| App launcher | Left column — the only cross-app navigation in the product |
| Breadcrumb trail | Center column, replaced wholesale when an app pushes its own |
| Organization switcher | Right column |
| Theme toggle | Right column — the single control for the whole console |
| Toast viewport | One stack for the console and every embedded app |
| Confirm modal | Centered on the viewport, not on a panel |
The corresponding rules for the app side are in Embedded apps.
Navbar
<div class="bg-navbar grid h-10 select-none grid-cols-3 grid-rows-1 border-b">
<div class="flex items-center justify-start pl-1.5">…launcher…</div>
<div class="flex items-center justify-center">…breadcrumbs…</div>
<div class="flex items-center justify-end">…org · theme…</div>
</div>h-10is a contract, not a style choice. Embedded apps size their own top bars to match it so the two rows read as one band of chrome.--navbaris its own token. It equals--backgroundin light and is the brand color in dark. Do not substitutebg-background.select-noneon chrome. Dragging a selection across a navbar is never intentional.- Client-pushed breadcrumbs replace the host-derived trail rather than appending to it. The app knows its own hierarchy; the host only knows the URL.
Host page shell
Host pages have no sidebar, so the shell is simpler than an app's — but the scroll contract is identical. The route root owns the scroll:
<div class="h-full overflow-auto p-8">
<div class="mx-auto flex max-w-4xl flex-col gap-6">
<header class="flex flex-col gap-1">
<h1 class="font-heading text-xl font-semibold tracking-tight">Settings</h1>
<p class="text-muted-foreground text-xs/relaxed">…</p>
</header>
…
</div>
</div>The console's own routes do not currently match that snippet — they use
space-y-6 for rhythm, a text-2xl title and a text-sm description, all of
which predate these guidelines and conflict with
Spacing rhythm and
The scale. The snippet above is the
target; the existing pages are the backlog.
Two differences from the app-side Page primitive are real and unresolved: host
pages use p-8 / max-w-4xl, apps use p-6 / max-w-5xl. These should
converge on the app-side values, since there are far more app pages than host
pages. Until that lands, do not mix the two within one surface.
Theme broadcast
The host owns the theme and must push every change to every connected app.
const next = theme === "light" ? "dark" : "light";
setTheme(next);
document.documentElement.classList.toggle("dark", next === "dark");
host.broadcastContextPatch({ theme: next });Three rules:
- Patch, never rebuild. Each channel merges the patch into its own context snapshot, which carries per-channel fields such as the mount path. Pushing a whole reconstructed snapshot from a toggle handler drops them.
- Persist only explicit choices. A user who has never touched the toggle keeps following their OS preference; writing a value on first load silently opts them out of it forever.
- Cross-origin iframes do not join a view transition. If the host crossfades
its own chrome between palettes, embedded apps still swap on their own bridge
schedule. Keep the crossfade short so the two do not visibly disagree, and
skip it entirely under
prefers-reduced-motion.
Framing an embedded app
The host is responsible for the app's first impression.
- Cover the iframe until the handshake resolves, then fade out. Use a delayed fade-in on the spinner so a fast handshake never flashes one.
- Use
--mutedfor the overlay, so the transition is into the app rather than out of a white flash. - Keep the overlay non-interactive while fading, and unmount it afterwards.
- The iframe itself is
size-full border-0with a--mutedbackground.
Where the host may be more expressive
Host pages carry a little more personality than app pages, and there is a line.
The app launcher uses a short staggered entrance on its tiles. The reasoning in the source is the rule:
An occasional page, so a little delight is allowed.
Rule. Entrance animation belongs to landing and navigational surfaces that a user sees briefly and infrequently. It does not belong on data-dense pages, tables, or anything a user returns to many times an hour — there, the animation becomes a delay between the user and their work.
Guard any such entrance with a reduced-motion variant, and keep it under 300ms.
Token ownership
apps/console/src/styles.css is the source of truth for every console app.
Rule. A change to that file is a change to every app's vendored copy. Changes must be additive where possible, and announced where they are not. Removing or repurposing a token breaks apps you cannot see from this repository.
The long-term fix is to publish the theme as a package so apps consume it rather than copy it. Until then, the sync obligation is real and manual — see Source of truth.