- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
Projects
import { PageHeader } from "@/components/ui/page-header";
export const PageHeaderDemo = () => (
<PageHeader
title={"Projects"}
description={"Manage your workspace projects."}
actions={
<a href="/projects/new" className="inline-flex min-h-9 items-center rounded-md border border-border px-3 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring">
Create project
</a>
}
/>
);Installation
Initialize shadcn in your React project, then install this component from the Vandor UI registry:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/page-header.json
Dependencies
Installed automatically by the registry CLI. For manual setup:
- cn
$ pnpm add cnUsage
import { PageHeader } from "@/components/ui/page-header";
<PageHeader title="Projects" description="Manage your workspace projects." />;PageHeader provides the identity and primary actions of a dashboard page. It is not the app's global navigation bar. Keep filters, tabs, summaries, and page content outside the header.
Examples
Simple heading
Projects
import { PageHeader } from "@/components/ui/page-header";
export const PageHeaderDemo = () => (
<div className="w-full min-w-0">
<PageHeader
title="Projects"
description="Manage your workspace projects."
/>
</div>
);Only title is required. The component renders an h1, supporting content, and a bottom divider. It adds no outer margin, background, width cap, sticky positioning, state, or animation. Remove the divider with className="border-b-0" if the surrounding layout already provides one.
Dashboard composition
Projects
Your project list belongs here, outside the header.
import Link from "next/link";
import { PageContainer } from "@/components/ui/page-container";
import { PageHeader } from "@/components/ui/page-header";
export const PageHeaderDashboardDemo = () => (
<PageContainer className="flex flex-col gap-6">
<PageHeader
title="Projects"
description="Manage access and review your active projects."
breadcrumb={
<nav aria-label="Breadcrumb">
<ol className="flex flex-wrap items-center gap-2">
<li>
<Link href="/workspace" className="underline underline-offset-4">
Workspace
</Link>
</li>
<li aria-hidden="true">/</li>
<li aria-current="page">Projects</li>
</ol>
</nav>
}
status={
<span className="rounded-md bg-muted px-2 py-1 text-xs">3 active</span>
}
actions={
<>
<Link
href="/projects/export"
className="inline-flex min-h-9 items-center rounded-md border border-border px-3 text-sm font-medium hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
Export
</Link>
<Link
href="/projects/new"
className="inline-flex min-h-9 items-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
Create project
</Link>
</>
}
/>
<p className="text-sm text-muted-foreground">
Your project list belongs here, outside the header.
</p>
</PageContainer>
);Install PageContainer separately for this example. It sets the page width and gutters; className="flex flex-col gap-6" provides spacing between the header and content. Example links are placeholders for your app's routes, not routes supplied by PageHeader.
The breadcrumb slot accepts your existing breadcrumb component without adding another navigation landmark. Supply a named nav, an ordered list, and aria-current="page" where appropriate. Plain text, as used by the playground control, is not automatically breadcrumb navigation.
Use status for a badge or other contextual content alongside the heading. Use actions for one or more links or buttons; fragments work without an extra wrapper. Actions follow the copy in DOM order. Below Tailwind's md breakpoint (768px by default), they appear below the heading and align to the start. On wider screens they align to the trailing side and wrap when needed. Breakpoint selection follows the viewport, not the width of an embedded preview.
Consumer setup and optional stories
Requires React 19, Tailwind CSS 4, and cn. Load your global Tailwind stylesheet with the semantic border and muted-foreground tokens. Example content and stories also use muted, foreground, primary, primary-foreground, and ring. There are no automatic Button, Badge, Breadcrumb, or PageContainer registry dependencies; supply or install your chosen components separately.
PageHeader has no provider, browser API, or client directive. It can be imported into a Next.js Server Component; interactive actions need their own appropriate client boundary. Installation follows the UI alias in components.json. For manual setup, install cn and copy the Source into your UI directory. Review overwrite prompts before replacing an existing PageHeader. Examples and playground adapters are not distributed.
Install PageHeader first, then optionally install its portable stories:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/page-header-stories.json
Use your existing @storybook/react installation with matching Storybook package versions (types and composition checked with 10.6.1). Discover **/*.stories.tsx in your UI directory and load your global Tailwind/theme stylesheet in the preview. No extra providers or addons are required. The stories-only item does not reinstall PageHeader or modify Storybook configuration. This project has no Storybook server; Storybook UI appearance is not verified here.
Behavior & Accessibility
The root is a native header, with attributes, handlers, and React 19 ref forwarded. title is the required heading text, not the native HTML tooltip attribute. Native children is not supported: use the named slots instead. Descriptions render inside a div so rich content such as a paragraph does not create invalid nested paragraphs.
Use one PageHeader as the page's primary h1 inside its main content. A header scoped inside main is not the global banner. The component does not label your main landmark, announce changing statuses, or move focus; add appropriate application-level accessibility where needed. Empty strings, null, undefined, and booleans omit optional slot wrappers; numeric zero remains visible. Empty fragments and arrays are not inspected for content.
Titles and descriptions wrap rather than truncate. Arbitrary wide custom slot content must provide its own responsive layout. Prefer actions directly inside a fragment so the actions container can wrap them. Preserve meaningful action names and native link/button semantics.
The playground's showActions is a composition control, not a public prop. Generated code uses the same text, optional slots, and sample action link as the preview. Reset restores these settings. The fixed sample link demonstrates layout, not project creation behavior.
Props
| Prop | Type / Description | Default |
|---|---|---|
| actions | ReactNodeOne or more action elements, trailing on desktop and below the heading on mobile. Wraps when space is limited. | Not set |
| breadcrumb | ReactNodeContent above the heading. Supply your own accessible breadcrumb navigation; PageHeader adds no nav wrapper. | Not set |
| className | stringClasses merged on the header root. No outer margin is added; border-b-0 removes the divider. | Not set |
| description | ReactNodeSupporting content below the heading, rendered in a div to allow rich content. | Not set |
| ref | Ref<HTMLElement>React 19 ref forwarded to the native header element. | Not set |
| status | ReactNodeOptional content next to the title, such as a badge. Does not add a live region. | Not set |
| title | stringPage heading rendered as an h1. This is not the native header title attribute. | Required |
PageHeader forwards native header attributes, handlers, and React 19 refs except children and title. Use its named slots; title sets the h1 text, not the native tooltip attribute.
Source
import { cn } from "cn";
import type { ComponentProps, ReactNode } from "react";
export type PageHeaderProps = Omit<
ComponentProps<"header">,
"title" | "children"
> & {
title: string;
description?: ReactNode;
breadcrumb?: ReactNode;
status?: ReactNode;
actions?: ReactNode;
};
const hasContent = (value: ReactNode) =>
value !== null &&
value !== undefined &&
typeof value !== "boolean" &&
value !== "";
export const PageHeader = ({
title,
description,
breadcrumb,
status,
actions,
className,
...props
}: PageHeaderProps) => (
<header
data-slot="page-header"
className={cn(
"flex min-w-0 flex-col gap-4 border-b border-border pb-6 md:flex-row md:items-end md:justify-between md:gap-6",
className
)}
{...props}
>
<div className="flex min-w-0 flex-1 flex-col gap-3">
{hasContent(breadcrumb) ? (
<div
data-slot="page-header-breadcrumb"
className="min-w-0 text-sm text-muted-foreground"
>
{breadcrumb}
</div>
) : null}
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h1 className="min-w-0 text-2xl font-semibold tracking-tight wrap-break-word">
{title}
</h1>
{hasContent(status) ? (
<div data-slot="page-header-status" className="min-w-0 text-sm">
{status}
</div>
) : null}
</div>
{hasContent(description) ? (
<div
data-slot="page-header-description"
className="max-w-[65ch] text-sm leading-relaxed text-muted-foreground wrap-break-word"
>
{description}
</div>
) : null}
</div>
{hasContent(actions) ? (
<div
data-slot="page-header-actions"
className="flex min-w-0 max-w-full flex-wrap items-center gap-2 md:justify-end"
>
{actions}
</div>
) : null}
</header>
);