Page header
The page h1, an optional subtitle, and the actions that belong to the page as a whole.
Code
The three render the same class names, the same ARIA and the same data attributes. That is checked when this site builds: a class in any of these samples that neither the markup contract nor the shipped stylesheet defines fails the build.
<div class="wsu-page-header">
<div>
<h1 class="wsu-page-header__title">Select courses</h1>
<p class="wsu-page-header__subtitle">Winter 2027, registration closes 8 November</p>
</div>
<div class="wsu-page-header__actions">
<button class="wsu-btn wsu-btn--outline" type="button">Export</button>
<button class="wsu-btn wsu-btn--primary" type="button">Add course</button>
</div>
</div><x-wsu::page-header
title="Select courses"
subtitle="Winter 2027, registration closes 8 November">
<x-slot:actions>
<x-wsu::button variant="outline" icon="download">Export</x-wsu::button>
<x-wsu::button variant="primary" icon="plus">Add course</x-wsu::button>
</x-slot:actions>
</x-wsu::page-header><PageHeader
title="Select courses"
subtitle="Winter 2027, registration closes 8 November"
>
<template #actions>
<Button variant="outline">Export</Button>
<Button variant="primary">Add course</Button>
</template>
</PageHeader>Why it works this way
- Exactly one h1 per page, and this is it. The shell renders no heading of its own. 1.x put the application name in an h1 in the header and the page title in another, so every page had two competing document outlines.
- On the Blade path the heading defaults to whatever the breadcrumb trail already implies, so pushing Breadcrumbs::pushAsHeading('Select courses') sets the last crumb and the h1 together and they cannot say different things.
Accessibility
- One h1 per page, always (2.4.6).
- The page title matching the h1 text is a page-authoring practice this component cannot enforce on its own, since <title> lives in the document head rather than in PageHeader: the Blade, raw-PHP and Vue examples all keep the two in sync, but the static-html example's own fixed <title> currently does not include the h1 text, confirmed live by reading document.title.
- Actions come after the heading in the DOM, in the order they are read.
Success criteria in brackets refer to WCAG 2.2. Every one of them is checked by axe-core in both themes at three viewport widths before this page can be published. See Accessibility for what the gates are.
Props
Blade
| Attribute | Default | What it does |
|---|---|---|
title | the breadcrumb heading | Falls back to Breadcrumbs::getHeading(). |
subtitle | the breadcrumb subheading | Rendered under the title. |
level | 1 | Only lower it if the page genuinely has its h1 elsewhere. |
Vue
| Prop | Type and default | What it does |
|---|---|---|
title | String, '' | Or use the default slot. |
subtitle | String, '' | Or use the subtitle slot. |
level | Number, 1 | Validated to 1 through 6. |
In the markup contract
contract/markup-contract.json is extracted from the two static templates and every package tests against it. This is what it records for .wsu-page-header.
| Template | Elements | Accessibility attributes | Uses |
|---|---|---|---|
| internal apps/examples/static-html/index.html | div | none recorded | 1 |
| public apps/examples/static-html/public-app.html | div | none recorded | 1 |