Skip to main content
WireKit
Copy for LLM

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.

A screen title with a description and one action

Roles

Who may do what. A role is a set of permissions you can hand to several people at once.

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.

A section header inside a screen that already has a title

Webhook endpoints

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.

Two actions beside a long title

Audit log

Every change, who made it and when.

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.

<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:

<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>

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:

<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>

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.

A title carrying a badge

Access requests 2

Two people are waiting for a decision.

A description slot does the same for the sentence underneath.

Personalization blocks

Three structures are resolvable through WireKit::personalize() / WireKit::scope(), 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

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.

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
size string|null null Heading size, when the level's own size is not the one this screen wants. The ladder is heading'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, 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 — the title itself, and the size ladder size reads from
  • Section — a full-width band with its own background, for a marketing page rather than an application screen
  • Button — what usually goes in actions

Updated in WireKit v2.54.0 (2026-09-19)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store