PageHeader
page-headerCombines back navigation, breadcrumbs, title, tags, actions, and footer tabs.
Usage
Minimalist (only title + action)
The most common list header: title on the left, main action button on the right.
User Management
<PageHeader
title="User Management"
extra={<Button variant="solid" size="sm">New user</Button>}
/>Breadcrumbs + subtitles + tags
breadcrumb is rendered above the title, and subTitle and tags are pasted to the right of the title.
Product List
Manage on-sale and off-the-shelf products<PageHeader
breadcrumb={<Breadcrumb items={[{ label: "Home", href: "/" }, { label: "Product List" }]} />}
title="Product List"
subTitle="Manage on-sale and off-shelf products"
tags={<Chip tone="brand" variant="soft" size="sm">128 For sale</Chip>}
/>Details page (return + Tabs footer + divider)
Pass in onBack to render the return arrow; footer always puts Tabs; bordered adds a divider line at the bottom.
Order #20260603-8821
6 items in total<PageHeader
onBack={() => router.back()}
breadcrumb={<Breadcrumb items={items} />}
title="Order #20260603-8821"
subTitle="6 items in total"
tags={<Chip tone="brand" variant="soft" size="sm">In progress</Chip>}
extra={<><Button variant="ghost" size="sm">Export</Button><Button variant="solid" size="sm">Edit</Button></>}
footer={
<Tabs defaultValue="detail">
<TabsList>
<TabsTab value="detail">Details</TabsTab>
<TabsTab value="items">Product</TabsTab>
<TabsTab value="logistics">Logistics</TabsTab>
</TabsList>
</Tabs>
}
bordered
/>Meta row
meta is the run of factual values under the title joined by a separator. Empty items are skipped, so a missing value never leaves an orphan separator behind.
Zhang San
- 330106…512
- Male
- 3 insurance periods
- 2 companies
- Latest employer: Hangzhou Meifeng Technology Co., Ltd.
<PageHeader
title="Zhang San"
meta={[
"330106…512",
"Male",
segments && `${segments} insurance periods`, // no value drops the whole item, leaving no orphan separator
companyCount ? `${companyCount} companies` : null,
"Latest employer: Hangzhou Meifeng Technology Co., Ltd.",
]}
/>When to use
Use PageHeader to establish a consistent top section for detail and admin pages: back action, breadcrumb, primary title, status tags, right-aligned actions, and optional footer tabs. Its slots can directly compose HulianUI Breadcrumb, Chip, and Tabs. Use Breadcrumb alone when only hierarchy is needed.
Import
import { PageHeader } from "@hulianui/ui"Props
| Name | Type | Default | Description |
|---|---|---|---|
| backLabel | string | "\u8fd4\u56de" | Accessible label for the back button. The built-in Chinese copy means “Back.” |
| bordered | boolean | false | Whether to render a bottom divider using <Separator/>. |
| metaSeparator | ReactNode | "·" | Separator placed between meta entries. It is decorative and gets aria-hidden automatically. |
| titleAs | ElementType | "h1" | Element the title renders as. The heading level belongs to the page; only the tag is handed over, the font size does not follow it (always 20px/28px). |
Also inheritsHTMLAttributes<HTMLElement>excepttitle, whose type is replaced withReactNode.
Events
| Event | Type | Description |
|---|---|---|
| onBack | () => void | When provided, renders a back-arrow button before the title and calls this handler on activation. A callback requires the consumer to be a client component. |
Slots
| Slot | Type | Description |
|---|---|---|
| title* | ReactNode | Primary title. |
| subTitle | ReactNode | Muted secondary title displayed inline after the primary title. |
| breadcrumb | ReactNode | Breadcrumb region above the title row, typically a HulianUI <Breadcrumb/>. |
| tags | ReactNode | Status indicators beside the title, such as <Chip/> or <Badge/>. |
| meta | ReactNode[] | Metadata row: the string of factual values under the title, joined by metaSeparator. The component inserts the separator between entries and skips empty ones. |
| extra | ReactNode | Actions on the right. It wraps below the title on narrow screens (viewport under 640px); on wider viewports no title length pushes it down, and a long title truncates instead. |
| footer | ReactNode | Footer region, commonly <Tabs/>. |
Example
// Minimal title and action
<PageHeader title="Users" extra={<Button variant="solid" size="sm">New user</Button>} />
// Complete header
<PageHeader
onBack={() => router.back()}
breadcrumb={<Breadcrumb items={[{ label: "Home", href: "/" }, { label: "Order details" }]} />}
title="Order #20260603-8821"
subTitle="6 items"
tags={<Chip tone="brand" variant="soft" size="sm">In progress</Chip>}
extra={<Button variant="solid" size="sm">Edit</Button>}
footer={<Tabs defaultValue="detail"><TabsList><TabsTab value="detail">Details</TabsTab></TabsList></Tabs>}
bordered
/>Handing the title tag back to the page (the header is not the top-level heading, or the title is an animated component):
// The page h1 lives elsewhere, so demote the header title to h2
<PageHeader titleAs="h2" title="Zhang San" />
// Animated title: the animation component drops to a span inside the tag chosen by titleAs
<PageHeader titleAs="h2" title={<AnimatedTitle as="span">Zhang San</AnimatedTitle>} />Metadata row (ID number, gender, insured periods, and so on):
<PageHeader
title="Zhang San"
meta={[
"330106…512",
"Male",
insuredPeriods && `${insuredPeriods} insured periods`, // an empty entry disappears, leaving no stray separator
companyCount ? `${companyCount} companies` : null,
latestEmployer && `Latest employer: ${latestEmployer}`,
]}
/>Usage guidelines
metaholds a series of parallel factual values. Keep the other slots for what they are: one sentence of supporting copy goes insubTitle, status markers intags, and block content such as tabs infooter.- Empty entries in
meta(null,undefined,false,"") are skipped, and the separator is inserted only between the entries that survive, so callers do not need tofilter(Boolean)first. The number0is a factual value ("0 companies") and is kept. - The metadata row renders as
<ul>/<li>with the separator in its ownaria-hiddendecorative item, so a screen reader announces list items instead of one long string glued together by middle dots. Stop hand-rollingspan + span::before { content: "·" }. - When migrating away from
span + span::before { content: "·" }, check the entries one by one instead of copying the row over. That selector really means "insert a dot only between adjacent rendered<span>elements", so wherever the old row mixed in a button, icon, or link (rendered as<button>,<svg>, or<a>) there was never a dot in production. Ametaentry, by contrast, is an array item: the separator goes between items regardless of what each one renders as. Porting a mixed row such as[idNumber, <CopyButton/>]verbatim adds a separator that was not there before, which is a real visual regression rather than a bug in this component (hulianui/hulian#247). - `extra` wraps based on the viewport, not on title length (#263). The left column is
flex: 1 1 0, so on a wide viewport no title, however long, pushes the actions to a second row; only a viewport under 640px makes them give way. The same problem surfaced first onCardHeaderin Card, where card width comes from the layout rather than the viewport -- so that component has no narrow-screen step at all. titleAshands over the tag, not the font size: after switching toh2the title is still 20px/28px. Adjust the size with a descendant selector onclassName(which lands on the outer<header>), rather than nesting an extra element insidetitlewhose utility classes override the parent size, which leaves two conflicting font-size declarations on one heading.titleis aReactNode, which conflicts withHTMLAttributes.title?: string; the native attribute is omitted from the type. Do not expect a stringtitleto pass through to the DOM.- The default back label follows
ConfigProvider, whilebackLabeloverrides it. PageHeader is therefore a client component; server components can still import and render it.
Related
Tabs · Breadcrumb · Pagination · Anchor · Affix · BackTop
Playground
Order #20260603-8821
6 items in total<PageHeader
onBack={() => router.back()}
breadcrumb={<Breadcrumb items={items} />}
title="Order #20260603-8821"
subTitle="6 items in total"
tags={<Chip tone="brand" variant="soft">In progress</Chip>}
extra={<Button>Edit</Button>}
footer={<Tabs>…</Tabs>}
bordered
/>