A page's title, with its trail, category, a sentence and actions.

Basic

PageHeader draws a page's header as this site draws its own: a Breadcrumbs trail, an eyebrow that names the category, the page's one h1, your actions at the title's end and a sentence in slate under them. PageColumn holds the page under it, so the content starts where the title does and ends where the actions end. The examples on this page are not this page's own header, so their titles are h2s, through headingLevel; this page keeps its one h1.

Example

The page under the header starts where its title does.

Follows the page theme and corners
import { Breadcrumb, Breadcrumbs, Button, PageColumn, PageHeader, Text } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function PageHeaderBasic(): ReactElement {
  return (
    <div style={{ width: '100%' }}>
      <PageHeader
        // An example on a docs page, which has its own h1; a page's own header keeps the default, 1.
        headingLevel={2}
        breadcrumbs={
          // Two trails share this docs page, so this one names its landmark apart; a page has one.
          <Breadcrumbs aria-label="Breadcrumbs, settings">
            <Breadcrumb href="/docs">Home</Breadcrumb>
            <Breadcrumb href="/docs/components/page-header">Settings</Breadcrumb>
            <Breadcrumb>Notifications</Breadcrumb>
          </Breadcrumbs>
        }
        eyebrow="Settings"
        title="Notifications"
        description="Choose what reaches you by email and what waits in the app."
        actions={<Button variant="secondary">Send a test</Button>}
      />
      <PageColumn>
        <Text tone="muted">The page under the header starts where its title does.</Text>
      </PageColumn>
    </div>
  );
}

A documents page

A page that takes files anywhere pairs a Page drop zone with its keyboard twin, an Upload FileButton in the header's actions. The trail goes through your RouterProvider. Turn the switch on and drag a file over the page, or press Upload.

Example

Changed

Nothing added yet.

Follows the page theme and corners
'use client';

import {
  Breadcrumb,
  Breadcrumbs,
  FileButton,
  PageColumn,
  PageDropZone,
  PageHeader,
  RouterProvider,
  Switch,
  Text,
} from '@snapshot-ui/react';
import { useRouter } from 'next/navigation';
import { useState, type ReactElement } from 'react';

const TYPES = ['application/pdf', '.pdf', 'image/*', '.heic'];

export default function PageHeaderDocuments(): ReactElement {
  const router = useRouter();
  const [isOn, setOn] = useState(false);
  const [added, setAdded] = useState<string[]>([]);
  const take = (accepted: File[]): void => {
    setAdded((names) => [...accepted.map((file) => file.name), ...names].slice(0, 5));
  };
  return (
    <RouterProvider
      navigate={(href) => {
        router.push(href);
      }}
    >
      <div style={{ width: '100%' }}>
        <PageHeader
          // An example on a docs page, which has its own h1; a page's own header keeps the default, 1.
          headingLevel={2}
          breadcrumbs={
            <Breadcrumbs>
              <Breadcrumb href="/docs">Home</Breadcrumb>
              <Breadcrumb href="/docs/components/upload-tray">Documents</Breadcrumb>
              <Breadcrumb>Receipts, 2026</Breadcrumb>
            </Breadcrumbs>
          }
          eyebrow="Documents"
          title="Receipts, 2026"
          description="Every receipt you uploaded this year, read and filed by month."
          actions={
            // The page zone's keyboard twin, in the header beside the title it uploads to.
            <FileButton acceptedFileTypes={TYPES} allowsMultiple onFiles={take}>
              Upload
            </FileButton>
          }
        />
        <PageColumn className="stack">
          <Switch isSelected={isOn} onChange={setOn}>
            Drop anywhere on this page
          </Switch>
          <Text size="small" tone="muted">
            {added.length === 0 ? 'Nothing added yet.' : `Added ${added.join(', ')}`}
          </Text>
        </PageColumn>
        {/* In an app, mount the page zone at the page's root, beside its header: it draws its overlay in the shell. */}
        {isOn ? (
          <PageDropZone
            title="Drop files to upload"
            description="To Receipts, 2026"
            acceptedFileTypes={TYPES}
            allowsMultiple
            onFiles={take}
          />
        ) : null}
      </div>
    </RouterProvider>
  );
}

Usage

  • PageHeader is static. It renders in a server component and ships no client JavaScript; a Breadcrumbs or a FileButton you put in it is a client part of its own.
  • The title is an h1, drawn at the 36px section step (28px under 768px). A page has one, so render one PageHeader per page. Where a page shows a header that is not its own, as a gallery of examples does, headingLevel lowers the heading and keeps the size.
  • It never sticks by itself. Put it in App shell's header slot, the first part of <main>, and it sticks with the shell's header, and the shell writes its height as --page-sticky-top once the page has loaded, so a Data table's header row and a Side panel stick under it. Before that, the table's header row sticks at the top; give the shell an estimate to keep the jump small, as the shell's page shows. At the top of the page's content it scrolls away. A stuck header with a description takes a phone's screen, so give the sticky one a title and actions only.
  • Actions sit at the end of the title's line, their bottom on the title's, and fall under the title only when it would keep less than 20rem beside them; under 768px they take a line of their own. A long trail wraps within the titles and a sentence takes its own line under the title and the actions, so neither moves them. Nothing is measured and nothing moves into a menu, so a page with many actions passes a Menu as the last one.
  • The header's fill spans its holder and its row is a page column: --container-page wide, 82.5rem with its padding, and centered past it, as this site's content column is. Put your page in a PageColumn beside the header, not around it; a column inside a column takes no second padding. Set --container-page on the root, or on the shell, to change both at once.
  • A Tailwind user writes the page width as max-w-page, or max-w-(--container-page).
  • The fill's blur makes the header the containing block of a position: fixed element inside it, which then sits within the header rather than the window, so render a popover or a dialog of its actions in a portal, as the library's own do.
import { AppShell, Breadcrumb, Breadcrumbs, PageColumn, PageHeader } from '@snapshot-ui/react';

<AppShell
  sidebar={<Nav />}
  header={
    <PageHeader
      breadcrumbs={
        <Breadcrumbs>
          <Breadcrumb href="/">Home</Breadcrumb>
          <Breadcrumb href="/documents">Documents</Breadcrumb>
          <Breadcrumb>Receipts, 2026</Breadcrumb>
        </Breadcrumbs>
      }
      title="Receipts, 2026"
      actions={<UploadButton />}
    />
  }
>
  <PageColumn>{/* The page. */}</PageColumn>
</AppShell>;

Props

PageHeader accepts every HTML attribute on its header, and PageColumn on its div.

actions
ReactNode

Buttons at the title's end: a FileButton, a primary Button, a Menu. They wrap under the title when the title would keep less than 20rem beside them.

breadcrumbs
ReactNode

A Breadcrumbs, above everything else.

className
string

description
ReactNode

A sentence under the title and the actions, in slate.

eyebrow
ReactNode

The category above the title, as an Eyebrow draws it: "Documents".

headingLevel
HeadingLevel

The title's heading level. A page has one h1, so keep the default; lower it only where a page shows a header that is not its own, as a gallery of examples does. It never changes the title's size.

Default: 1

title
ReactNode

The page's title, its h1.

Accessibility

The header is a header element holding the page's one h1. The shell's header slot is the first part of <main>, so there, as at the top of <main> in any page, it is a plain header, not a banner landmark: the skip link lands on the title, and the h1 is the first heading in the main landmark. The trail is a navigation landmark of its own, and the eyebrow is a label, never a heading. The header takes no focus and has no keys; its actions are in the page's tab order after the trail, in the order they are drawn. A control that focus moves to never stays under the stuck header, as the shell pads the page's scroll by its height. For a reader who asks for less transparency or more contrast, and in forced colors, the header draws its background whole with no blur.

Design rules

What keeps page headers in the system.

Name the page in the title, and the place it sits in the trail and the eyebrow.

Keep the sentence to one line of what the page is for.

End many actions with a Menu.

Never render two titles: a page has one h1.

Never draw a rule beneath the header.

Never put a description in a header that sticks on a phone.

esc

↑ ↓ to move↵ to open