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.
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.
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.
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.
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> levelis1by default. A screen with two<h1>elements is the failure this default prevents — putlevel="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.