Display
Page Header
The app-shell page header — breadcrumb slot, icon, title, status meta, actions and an optional controlled tab strip, all responsive to a narrow container.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/page-header.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "PageHeader" component.
Contract
- Export a forwardRef<HTMLElement, PageHeaderProps> rendering a <header>;
spread remaining native header props on the root, merge className via cn().
- Props: title (string, required), description?: ReactNode, icon?: ReactNode,
breadcrumb?: ReactNode (slot — the consumer drops in their own breadcrumb
component here, e.g. this registry's Breadcrumbs), meta?: ReactNode
(rendered inline right after the title — status pills, version tags),
actions?: ReactNode (top-right action area), tabs?: { value: string;
label: string; badge?: number | string; disabled?: boolean }[], value?:
string (controlled selected tab), onValueChange?: (value: string) => void,
border?: boolean (default true), sticky?: boolean (default false), size?:
"sm" | "md" (default "md"), titleAs?: "h1" | "h2" | "h3" (default "h1").
Behavior
- Title row layout: icon + title + meta form one flex item on the left,
actions form a second flex item on the right, both inside a flex-wrap row.
The left item is flex-1 min-w-0 (it has no floor — it shrinks first); the
actions item keeps its natural content size. Once actions no longer fit
next to the shrunk-to-minimum title, the whole actions block wraps to its
own line below the title row — it never squeezes into a sliver, and the
title never gets pushed into overflow.
- title renders as line-clamp-2 (plus break-words) so an excessively long
string clamps instead of growing the header indefinitely; description
(when present) is also line-clamp-2 under the title row.
- Tabs render as a real APG tablist under the title row: role="tablist" +
role="tab" per item, aria-selected on the active tab, roving tabindex
(only the active — or first enabled — tab has tabIndex 0, every other tab
is -1). ArrowRight/ArrowLeft move focus+selection to the next/previous
ENABLED tab (wrapping around), Home/End jump to the first/last enabled
tab. disabled tabs use the native disabled attribute — unfocusable,
unclickable, and skipped by arrow-key traversal. Clicking a tab or moving
focus onto one calls onValueChange(tab.value); PageHeader holds no
internal selection state, it's fully controlled by value/onValueChange.
- PageHeader renders the tablist only — it never renders a tabpanel. Wire
aria-controls yourself: give each panel a stable id and either point the
tab's aria-controls at it (fork this component to add that prop) or just
give the panel aria-labelledby pointing at the tab, whichever matches how
your app already associates tabs with content.
- sticky adds position/background/blur only (sticky top-0 z-30
bg-background/95 backdrop-blur) — it never sets overflow-hidden, so
focus-visible rings on interior controls (like the tab strip) are never
clipped while the header is pinned.
- The tab strip's scroll container is horizontally scrollable
(overflow-x-auto) with a small negative-margin/padding pair around it so
focus rings on the first/last tab aren't cut off by the scroll clip.
Rendering & styling
- Semantic tokens only: text-foreground for the title, text-muted-foreground
for description/icon/inactive tabs, border-primary for the active tab
underline, bg-muted for the tab badge pill, bg-background/95 for the
sticky backdrop. cn() merges consumer className throughout.
- size="sm" vs "md" only changes padding, title text size and icon size —
never structure.
- No decorative motion in this component (tab underline is a plain
transition-colors, not a sliding indicator), so there's no
prefers-reduced-motion branch to write.
Customization levers
- Heading level: titleAs swaps the rendered tag (h1/h2/h3) for pages that
nest this under another h1, with zero visual change.
- Tab visual style: swap the border-b-2 underline for a pill/segment look —
only the active/inactive class branches change, the roving-tabindex
engine is untouched.
- Density: size adds a third value (e.g. "lg") the same way sm/md are
defined — one more entry in the SIZE map.
- Sticky offset: change top-0 to a fixed offset (top-14) when the header
sits below another sticky bar (e.g. a global app topbar).
- Meta/actions content: both are plain ReactNode slots — status badges,
avatar stacks, a dropdown menu, a search box, anything the page needs;
PageHeader only owns the layout and wrap behavior around them.Concepts
- Actions wrap before title clamps — the title/meta block is the
flex-1 min-w-0item with no floor, so it always shrinks first; once the actions block genuinely can't fit beside it, the whole actions group relocates below the title row instead of either side getting squeezed into a sliver. - Controlled, panel-less tablist —
value/onValueChangemake the tab strip fully controlled (PageHeader keeps no selection state of its own), and it deliberately renders notabpanel— that DOM belongs to whatever content the tabs are switching. - Roving tabindex — only the active (or first enabled, as a fallback) tab is reachable by Tab; arrow keys move both focus and selection among the enabled tabs only, so a
disabledtab is invisible to keyboard traversal, not just visually dimmed. - Sticky without occlusion —
stickyonly adds positioning, a translucent blurred background and a z-index; it never introducesoverflow-hidden, so a focus ring on a tab or action button stays fully visible even while the header is pinned to the top of a scrolling container. - Clamp, not collapse — both
title(2 lines) anddescription(2 lines) useline-clamp, keeping the header's height predictable regardless of how long a caller's string is, instead of letting either grow the header unboundedly.
JSON Viewer
A collapsible, searchable JSON tree viewer with type-colored values, per-node copy value/path, and safe rendering of circular refs and every JS edge-case value.
Bulk Action Bar
A floating toolbar that mounts once rows are selected — inline actions, an overflow "More" menu, and a live-announced count.