Skip to main content
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.

Without it, screens that each build their own title drift: an h2 inside a content card on one, an h1 at size="lg" in a hand-built frame on the next, an h1 at size="2xl" inside a container on a third, and pages that sit next to each other in the same navigation title themselves at three different sizes.

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.

Beside a title with no description, the actions add nothing to the header's height. The buttons sit centered on the title and stand a little over and under its line, so the content below starts at the same distance from the title whether the header has actions or not. Once the actions wrap under the title, the header holds the title, the row gap and the actions.

Actions beside a title without a description

Open items

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 long name, for instance, can be kept to one line:

A long title kept to one line

Winter jacket with a detachable hood and a quilted lining

The name is cut where the header runs out of room.

A description slot does the same for the sentence underneath.

Status beside the title

A status or a count belongs next to the name it describes, and not inside the heading: in the default slot a badge becomes part of the title, and a screen reader reads "Winter jacket Active 3 variants" as the name of the screen. The meta slot draws it in the title's row, outside the heading. It wraps with the title and never drops into the actions:

A record's status beside its name

Winter jacket

Active 3 variants

Last changed an hour ago.

Sticking to the top

sticky keeps the header at the top of the box the page scrolls in, and below a strip such as an announcement banner with strip, which writes its height to --wk-strip-inset. In the flow of the page it looks as it does without, with as much space above its content as below it. Once it rests at the top and the content scrolls under it, a bottom border fades in and sets the two apart. Under reduced motion the border appears without fading.

A header that rests at the top of a scrolling list

Orders

The orders of the last 30 days.

Order 1042, Ada Lovelace, shipped on Monday.

Order 1041, Grace Hopper, waiting for payment.

Order 1040, Alan Turing, shipped on Friday.

Order 1039, Katherine Johnson, returned.

Order 1038, Edsger Dijkstra, shipped on Thursday.

Order 1037, Barbara Liskov, shipped on Wednesday.

Order 1036, Donald Knuth, waiting for stock.

Order 1035, Margaret Hamilton, shipped on Tuesday.

Order 1034, John Backus, shipped on Monday.

Order 1033, Frances Allen, canceled.

An element that takes the focus, or that a fragment link brings into view, comes to rest below the resting header rather than under it, in the page and in a box alike (keeping the focus clear of a pinned bar). The resting state is data-wk-stuck on the header, for a style of your own. Three knobs reach the header without replacing a block: --wk-page-header-rest-gap sets the space above and below its content, --wk-page-header-top the line it sticks to, and --wk-page-header-bleed how far it reaches past its box at the sides. A box that scrolls with a padding of its own holds a sticky element inside that padding, so the content shows above the resting header; a negative --wk-page-header-top of the same size lets the header cover it. The content also shows beside the header, in the padding at the sides; --wk-page-header-bleed the size of that padding lets the header reach the box's edges there too, with its own content where it was:

<main class="overflow-y-auto p-6">
    <x-wirekit::page-header title="Orders" sticky style="--wk-page-header-top: -1.5rem; --wk-page-header-bleed: 1.5rem" />
</main>

Personalization blocks

Four 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
page-header.sticky The sticky header, present only with sticky — its position, layer, background, padding and the border that shows while it rests

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.

Beside actions and no description, the column also grows 999 times faster than the frame that holds the actions (grow-[999]), so the free space stays with the title. A replacement should keep a growth like it; with a plain grow, the title shares the free space with the actions and wraps sooner.

These blocks 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, kept to the wide reading measure of text, about 78 characters. 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
sticky bool false Stick to the top of the box the page scrolls in; resting there, a bottom border fades in — see Sticking to the top
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
meta Short facts beside the title and outside the heading, a status badge or a count. It wraps with the title — see Status beside the title

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.65.0 (2026-10-05)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store