Gesso
Layout

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

Active

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

Draft

Opened Jan 2026

Overview of the project.

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

Draft
<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

Draft
Owner
Maya Chen
List
Subscribers

Props

PageHeader takes every <header> attribute except title, plus these.

PropTypeDefaultWhat it does
titleReactNodeThe 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.
subtitleReactNodeA line under the title.
badgeReactNodeA status after the title, such as a Chip or Badge. It wraps under the title when it does not fit.
titleAccessoryReactNodeA control right after the title and ahead of the badge.
onBack() => voidShows 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.
backLabelstring"Back"The stacked button's text and name, and the inline arrow's name and tooltip.
breadcrumbReactNodeReplaces the stacked back button. The inline arrow still shows.
actionsReactNodeButtons at the end of the title row. They fold into the menu on narrow headers when menuActions is set.
primaryActionReactNodeThe main verb, after actions. It stays visible when the rest fold.
menuActionsReactNodeMenuItems that mirror actions, shown behind a "More actions" kebab under 36rem.
tabsReactNodeA TabsList, flush on the bottom edge.
childrenReactNodeRendered 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 h2 by default. A page needs one h1, so pass as="h1" on the page's own header, and keep size for the look. Use as="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, so backLabel is 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 actions are hidden with display: none, which removes them from the tab order and the accessibility tree. Pass the same actions to menuActions, 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

On this page