Decided 2026-10-09 by the founder: "drop slogans, title labels, every noise, and set a standard rule; take very minimum space." Component:
apps/seller-dashboard/src/components/page-header.tsx(PageHeader,PageAction,HeaderCount).
Why
The header above every page already says where you are: the service button plus the highlighted page tab. An audit of about 70 page headers found that each page then repeated that:
- No shared component. Every page hand-built its own header.
- Seven title sizes (
text-xlup totext-5xl), in five hand-built styles. - Heights from about 60px to 300px. Content started far down the screen.
- Repetition: an eyebrow restating the nav ("FORMS · INBOX"). The Forms page said "Forms" three times.
- Slogans as titles ("Pulse of your library.", "Deliver / ability.").
- Toolbars repeating nav tabs as buttons.
- Copies: several hand-made copies of the stat strip and the live dot.
The rule
A page header is one row, 44px. It contains only:
[← Parent /] Page name [count or status] [secondary] [secondary] [Primary]
tabs (only when the page has its own sections)- Title. On a page reached from a nav tab, the title is exactly that tab's label (from
service-registry.ts). On a record page it is the record's name, after a← Parentback link. Anywhere else, a short plain name. - Meta. At most one small item next to the title: a
HeaderCountor a status chip. - Actions. Right-aligned: at most two
PageActionsecondaries, then onePageAction primary. Everything uses the same size and the dashboard's editorial button style. - Tabs. Only a page's own sections go in the
tabsslot: a form's Build/Share/Settings, a collection's tabs, Billing, Team. The header then becomes a third header row, built like row 2: full-bleed, the page name on the left, a divider, then the tabs, actions on the right. UseHeaderTabsList/HeaderTabsTrigger(exported frompage-header.tsx): plain underline tabs, the same look as row 2. Never the grey pillTabsList, never icons in these tabs. On phones the tabs drop to their own line under the name.
Section headings inside a page are <SectionHead title="…" /> (16px, semibold). The old
numbered eyebrow ("01 — Identity") is no longer rendered anywhere.
Never in a page header:
- an eyebrow, kicker or "Service · Page" label
- a slogan or tagline
- a description or subtitle
- a live dot or decorative status
- a stat strip (stats are the page's first row, directly under the header)
- filters, search, range toggles or steppers (they live in the page, directly under the header)
- a button that links to a nav tab of the same service
- a divider (unless there are tabs)
No page header on Home either: the coral "⌂ Home" button names it, and it starts straight
with "Needs you" (a visually hidden <h1>Home</h1> keeps the outline right).
No page header at all on full-screen consoles and editors. They keep their own toolbars:
| Kind | Screens |
|---|---|
| Consoles | Mailbox inbox, Chatbot Sandbox, AI Copilot, Contacts Playground (chat) |
| Editors | blog editor, newsletter composer and template editor, content composer |
Spacing. PageHeader brings its own pt-2 mb-4, so the header-to-content gap is 16px
everywhere. Don't wrap it in extra padding.
Full width. Every page uses the full content width, like the service pages. Never cap a page
wrapper (max-w-3xl, max-w-5xl…) and never centre it with mx-auto: a capped page either hugs
the left with an empty right half or floats away from the tabs above it. Only text blocks
(a paragraph, an empty state) may cap their own line length.
No back link to a tab. back is for record pages (a key, a form, a campaign). A page that
already has a tab in row 2 never gets "← Parent": the tab row is the way back.
Status
Rolled out 2026-10-09 to every page in the dashboard shell: Forms, Engagement, Collections, Flows,
Chatbot, AI Brain, Contacts, Blog, Newsletter, Mailbox, Content Engine, Media, Dashboard,
Integrations and Settings. The shared StatStrip now sizes to its cells.
The account-settings area (/settings, profile, security, notifications, appearance,
account) followed the same day. Every page with a header in the app now uses PageHeader. The
exceptions are by design: consoles, editors, sign-in/invite, and the legal pages.
Adding a page
<PageHeader
title="Campaigns"
meta={<HeaderCount value={campaigns.length} label="campaigns" />}
actions={
<>
<PageAction>Import</PageAction>
<PageAction primary asChild>
<Link href="/dashboard/newsletter/campaigns/new">New campaign</Link>
</PageAction>
</>
}
/>
<StatStrip>…</StatStrip> {/* stats, if any: first row of the page */}Record page with its own sections:
<Tabs value={active}>
<PageHeader
back={{ href: "/dashboard/forms", label: "Forms" }}
title={form.name}
meta={<StatusChip status={form.status} />}
actions={<PageAction>Open hosted page</PageAction>}
tabs={
<HeaderTabsList aria-label="Form sections">
<HeaderTabsTrigger value="build" asChild><Link href="…/build">Build</Link></HeaderTabsTrigger>
…
</HeaderTabsList>
}
/>
</Tabs>