Page header
The title bar of a page, with back, a title, a badge, a subtitle, actions that fold into a menu, and tabs.
Page header is the bar at the top of a page or a panel. It holds the title and, beside it, the things that belong to the title: a way back, a Chip or Badge for status, a subtitle, and the page's actions. Its actions fold into a Menu when the header gets narrow, and a row of tabs can sit flush on its bottom edge.
Projects
12 open · last updated today
Usage
import { Chip } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { PageHeader } from '@/components/ui/page-header';
<PageHeader
as="h1"
title="Projects"
badge={<Chip>Active</Chip>}
subtitle="12 open"
onBack={() => history.back()}
actions={
<>
<Button variant="outline">Export</Button>
<Button>New</Button>
</>
}
/>The slots are independent, so pass only the ones the page needs. The header is min-h-[60px]
with px-8 py-4, and its title is text-heading-4 font-display, or text-heading-3 with
size="h3". The subtitle is text-body-sm in text-fg-muted. Action buttons should be the
default size and not sm.
Actions on narrow headers
Put the main verb in primaryAction and the rest in actions. When the header is under 36rem
wide, actions fold behind a kebab and primaryAction stays beside it, both on the title's
line. The kebab is a Menu, so pass the same actions again as
menuActions, written as MenuItems. The header measures its own width and not the window's, so
one squeezed by an open sidebar folds the same way as one on a phone.
<PageHeader
title="Projects"
actions={<Button variant="outline">Export</Button>}
primaryAction={<Button>New</Button>}
menuActions={<MenuItem onSelect={exportProjects}>Export</MenuItem>}
/>Without menuActions, nothing is hidden: the actions wrap onto their own line under the title,
as in the tabs example below. Either way, a badge that no longer fits beside the title wraps
under it, and a title too long for the row truncates.
Tabs sit on the bottom edge
Wrap the header in Tabs and pass a TabsList into tabs.
The header drops its bottom padding and the list sits flush on the bottom edge. Stretch the list
with className="w-full" if its hairline should span the header.
Project atlas
Opened Jan 2026
Back
onBack renders the way back. By default it is a ghost Back button stacked above the title.
With backStyle="inline" it is a 40px arrow IconButton
to the left of the title, pulled out so the arrow itself lines up with the header's inset. Use
inline when the header is a single band, such as a flow or a detail page. backLabel is the
stacked button's text and the inline arrow's name and tooltip, so say where it goes, "Back to
Campaigns" and not "Back". breadcrumb replaces the stacked button but not the inline arrow, so a
Breadcrumb and an inline arrow can both show.
titleAccessory sits right after the title and ahead of the badge, for a control that acts on
the title itself, like a rename pencil.
Spring newsletter
<PageHeader
title={campaign.name}
onBack={() => router.push('/campaigns')}
backStyle="inline"
backLabel="Back to Campaigns"
titleAccessory={<IconButton icon={PencilSimple} label="Rename campaign" />}
badge={<Chip>Draft</Chip>}
actions={<Button variant="outline" onClick={sendTest}>Send test</Button>}
primaryAction={<Button onClick={next}>Continue</Button>}
menuActions={<MenuItem onSelect={sendTest}>Send test</MenuItem>}
/>Title level and children
title takes any node, so it can carry its own markup, but keep it short enough to truncate
cleanly. It renders as an h2 unless you pass as, which takes h1 through h5 or p. A
page's own header should pass as="h1", and one inside a panel can stay at the default.
size changes only the look, h4 or h3, and never the element.
children render below the title row and above tabs, for something that belongs to the header
but is not a subtitle, like a row of properties.
Spring newsletter
- Owner
- Maya Chen
- List
- Subscribers
Props
PageHeader takes every <header> attribute except title, plus these.
| Prop | Type | Default | What it does |
|---|---|---|---|
title | ReactNode | The heading. It truncates to one line. | |
size | "h3" | "h4" | "h4" | The title's type step, text-heading-3 or text-heading-4. |
as | "h1" | "h2" | "h3" | "h4" | "h5" | "p" | "h2" | The element the title renders as. |
subtitle | ReactNode | A line under the title. | |
badge | ReactNode | A status after the title, such as a Chip or Badge. It wraps under the title when it does not fit. | |
titleAccessory | ReactNode | A control right after the title and ahead of the badge. | |
onBack | () => void | Shows the back control and runs this when it is pressed. | |
backStyle | "stacked" | "inline" | "stacked" | A Back button above the title, or an arrow to its left. |
backLabel | string | "Back" | The stacked button's text and name, and the inline arrow's name and tooltip. |
breadcrumb | ReactNode | Replaces the stacked back button. The inline arrow still shows. | |
actions | ReactNode | Buttons at the end of the title row. They fold into the menu on narrow headers when menuActions is set. | |
primaryAction | ReactNode | The main verb, after actions. It stays visible when the rest fold. | |
menuActions | ReactNode | MenuItems that mirror actions, shown behind a "More actions" kebab under 36rem. | |
tabs | ReactNode | A TabsList, flush on the bottom edge. | |
children | ReactNode | Rendered below the title row and above the tabs. |
Accessibility
- It renders a
<header>. Inside a page's<main>that is not a landmark, so it adds no banner for a screen reader to jump to. - The title is a real heading, an
h2by default. A page needs oneh1, so passas="h1"on the page's own header, and keepsizefor the look. Useas="p"when the title is not a heading. - The stacked Back button shows
backLabel, "Back" unless you set it, and that text is its name, so what a voice user sees is what they say. The inline arrow has no text, sobackLabelis its only name and its tooltip. - The folded actions are behind an IconButton named
"More actions" that opens a Menu, so they stay reachable by keyboard. At that width the
inline
actionsare hidden withdisplay: none, which removes them from the tab order and the accessibility tree. Pass the same actions tomenuActions, or a narrow header loses them. - The stacked Back button, the inline arrow, and the kebab are at least 44px tall on a touch screen, from the Button they are built on. The header has no motion of its own.
When to use something else
- A Section heading for a titled block inside a page. A page header names the whole page or panel, and a Section names one run of content under it.
- A Card header when the title belongs to a framed group of content and not to the page.
- Breadcrumb, passed to
breadcrumb, when the page sits deep in a hierarchy and the way back is a path. - Tabs on their own, without a header, when there is no title to show. The Issues example uses a page header over a list.
Install
shadcn add @gesso/page-header