---
title: Page Header
description: The title of a screen, an optional sentence under it, and the actions that belong to it.
visibility: guest
draft: false
---

# Page Header

Every screen in an application starts the same way: a title, sometimes a sentence explaining what the screen is for, and sometimes a button or two that act on the whole screen. `page-header` is that row, so the thirteenth screen looks like the first one.

It was reported from a starter kit whose screens had drifted three ways — an `h2` inside the content card, an `h1` at `size="lg"` in a hand-built frame, and an `h1` at `size="2xl"` inside a container. The cap heights measured 12, 14 and 17 pixels on pages that sit next to each other in the same navigation.

:::preview{title="A screen title with a description and one action"}
<x-wirekit::page-header
    title="Roles"
    description="Who may do what. A role is a set of permissions you can hand to several people at once."
>
    <x-slot:actions>
        <x-wirekit::button intent="primary">New role</x-wirekit::button>
    </x-slot:actions>
</x-wirekit::page-header>
:::

## The title is the document's heading

`level` is `1` by default, because a screen has one title and that title is the document's heading. Set it lower where the header is not the page's own — a panel inside a screen that already has one.

:::preview{title="A section header inside a screen that already has a title"}
<x-wirekit::page-header level="2" title="Webhook endpoints" description="Where we send events." />
:::

## Actions drop under the title when the row runs out

The actions sit beside the title while there is room for both. Below that they take their own line, and they wrap among themselves rather than running off the edge — which is the case a phone shows and a desktop never does.

The break is not a breakpoint: the title column asks for `16rem`, so the row breaks when the title and the actions together no longer fit, whatever the screen is doing around them.

:::preview{title="Two actions beside a long title"}
<x-wirekit::page-header
    title="Audit log"
    description="Every change, who made it and when."
>
    <x-slot:actions>
        <x-wirekit::button intent="neutral" surface="outline">Export</x-wirekit::button>
        <x-wirekit::button intent="primary">Filter</x-wirekit::button>
    </x-slot:actions>
</x-wirekit::page-header>
:::

## Naming the heading from somewhere else

`heading-id` puts an id on the heading, which is what lets something else point at it: a dialog names its title through `aria-labelledby`, and a skip link needs a target to jump to.

```blade
<x-wirekit::modal aria-labelledby="settings-title">
    <x-wirekit::page-header heading-id="settings-title" title="Settings" />
</x-wirekit::modal>
```

## Making the break a decision

The actions sit beside the title while there is room and drop under it when there is not. Where
that happens depends on the **label**: the title column asks for 16rem, and the row wraps once
16rem plus the gap plus the actions no longer fit. On a 375px phone that sum came to roughly 351px
against 343px of content column — so the actions dropped, with about 8px to spare. Shorten the
label, pick a narrower face or use a `sm` button and they stay beside the title, with a two-line
description living in 16rem.

`stack-below` takes that decision away from the label:

```blade
<x-wirekit::page-header title="Roles" stack-below="sm">
    <x-slot:actions>
        <x-wirekit::button size="sm">New</x-wirekit::button>
    </x-slot:actions>
</x-wirekit::page-header>
```

::: warning
**The width is the header's own, not the window's, and the scale says so.** A page header lives
inside a content column, and beside a sidebar that column is far narrower than the viewport — a
viewport breakpoint therefore does not fire where it matters. `stack-below` reads Tailwind's
**container** ladder instead, which shares its names with the viewport one and not its meanings:
`stack-below="sm"` is 24rem of **header**, where a viewport `sm` is 40rem of **window**. Pick the
size from how wide the header itself gets, never from a screen width.
:::

## Sending focus back to the title

A dialog returns focus to whatever opened it. When that control is **gone** by the time the dialog
closes — a delete confirmation that sits in the row it deletes — there is nothing to return to, and
the page title is the nearest honest place. Point the dialog there with `focus-return-to`, and give
the heading a `tabindex` so it can receive focus at all:

```blade
<x-wirekit::page-header heading-id="roles-heading" heading-focusable title="Roles" />

<x-wirekit::alert-dialog focus-return-to="#roles-heading">
    …
</x-wirekit::alert-dialog>
```

::: warning
**Do not rely on the overlay to add it for you.** WireKit's overlay code does set `tabindex="-1"`
on a focus target that has none, but that is a runtime attribute: it is absent from the template a
Livewire morph patches against, so the next server round trip can take it away again — silently,
because nothing about the markup looks wrong afterwards. Declare it with the prop and the morph
keeps it.
:::

It is off by default on purpose. A `tabindex` is a statement about focus order, and a screen that
never sends focus to its title should not make one.

## Markup in the title

The default slot takes the same place as `title` and wins when both are given — a slot can carry what a prop cannot.

:::preview{title="A title carrying a badge"}
<x-wirekit::page-header description="Two people are waiting for a decision.">
    Access requests <x-wirekit::badge intent="warning">2</x-wirekit::badge>
</x-wirekit::page-header>
:::

A `description` slot does the same for the sentence underneath.

## Personalization blocks

Three structures are resolvable through [`WireKit::personalize()`](/customization) / [`WireKit::scope()`](/customization), and a config override under `components.page-header.classes.{block}` reaches the same keys:

| Block key | Applies to |
| --- | --- |
| `page-header.base` | The header row itself — its container query, wrapping, alignment and gap |
| `page-header.column` | The title column — the block holding the heading and the description, including the `16rem` basis that decides when the actions drop to their own line |
| `page-header.actions` | The actions row — its own wrapping and the gap between buttons |

::: tip
Reach for `column` when the break is landing in the wrong place. The basis is what decides it: the column asks for `16rem`, so anything narrower than that plus the actions puts them on their own line. Widening or narrowing that one value moves the breakpoint without touching a media query.
:::

::: warning
These three exist so a call site never has to write a descendant selector into this component's markup. A rule like `.wk-page-header > div:first-child { … }` works today and breaks on the next refactor without telling you — the block keys are the supported way to say the same thing.
:::

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `title` | `string\|null` | `null` | The screen title. The default slot says the same thing and wins when both are given |
| `level` | `int` | `1` | Heading level, `1` to `6`. A screen has one title, and that title is the document's heading |
| `description` | `string\|null` | `null` | A sentence under the title. A `description` slot takes the same place when the text needs markup |
| `heading-id` | `string\|null` | `null` | The heading's id, so a dialog or a skip link can name it |
| `heading-focusable` | `bool` | `false` | Write `tabindex="-1"` on the heading, so focus can be sent to it |
| `stack-below` | `string\|null` | `null` | Put the actions on their own line below a width of the **header**: `'sm'`, `'md'`, `'lg'`, `'xl'`, `'2xl'` or `'3xl'` on Tailwind's **container** ladder — see [Making the break a decision](#making-the-break-a-decision) |
| `size` | `string\|null` | `null` | Heading size, when the level's own size is not the one this screen wants. The ladder is [heading](heading.md)'s |
| `scope` | `string\|null` | `null` | Theme scope, as everywhere else |

## Slots

| Slot | Description |
|---|---|
| default | The title, when it carries markup |
| `actions` | Controls that act on the whole screen. They drop under the title when the row runs out and wrap among themselves |
| `description` | The sentence under the title, when it carries markup |

## Accessibility

- The title renders through [heading](heading.md), so it is a real `<h1>`–`<h6>` and not a styled `<div>`
- `level` is `1` by default. A screen with two `<h1>` elements is the failure this default prevents — put `level="2"` on a header that sits inside a screen which already has its own title
- The actions are ordinary controls: they keep their own labels, their own focus rings and their own tab order, in the order the markup puts them

## Keyboard Interaction

The header renders no interactive element of its own — it is a title, a sentence and whatever you put in `actions`. Those controls keep their own keyboard behavior and their own place in the tab order, in the order the markup puts them.

## See Also

- [Heading](heading.md) — the title itself, and the size ladder `size` reads from
- [Section](section.md) — a full-width band with its own background, for a marketing page rather than an application screen
- [Button](button.md) — what usually goes in `actions`
