# PageHeader

A responsive dashboard heading with supporting content, breadcrumbs, status, and actions.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```tsx
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 [#installation]

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/page-header.json
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install cn
```

## Usage [#usage]

```tsx
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 [#examples]

### Simple heading [#simple-heading]

<ComponentPreview name="page-header-demo" title="page-header-demo.tsx">
  <PageHeaderDemo />
</ComponentPreview>

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 [#dashboard-composition]

<ComponentPreview name="page-header-dashboard-demo" title="page-header-dashboard-demo.tsx">
  <PageHeaderDashboardDemo />
</ComponentPreview>

Install [PageContainer](/docs/components/page-container) 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 [#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:

```bash
npx 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 [#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 [#props]

| Prop        | Type              | Default  | Description                                                                                                    |
| ----------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| actions     | ReactNode         | Not set  | One or more action elements, trailing on desktop and below the heading on mobile. Wraps when space is limited. |
| breadcrumb  | ReactNode         | Not set  | Content above the heading. Supply your own accessible breadcrumb navigation; PageHeader adds no nav wrapper.   |
| className   | string            | Not set  | Classes merged on the header root. No outer margin is added; border-b-0 removes the divider.                   |
| description | ReactNode         | Not set  | Supporting content below the heading, rendered in a div to allow rich content.                                 |
| ref         | Ref\<HTMLElement> | Not set  | React 19 ref forwarded to the native header element.                                                           |
| status      | ReactNode         | Not set  | Optional content next to the title, such as a badge. Does not add a live region.                               |
| title       | string            | Required | Page heading rendered as an h1. This is not the native header title attribute.                                 |

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 [#source]

```tsx
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>
);

```
