Page header

Every page starts with a page header (UX guide §3.2): an h1 with the page title, an optional one-line meta ("3 warten auf Freigabe · Formular online") and at most one primary action plus up to two secondary ones. Further actions go into an overflow menu composed by the app.

Detail pages get a back link to their parent list, named after it ("Mitglieder"), not "Zurück" and not browser history.

The title is the target of focus after a route change: render gives the h1 the id {id} (default page-title) and tabindex="-1", so the router can call document.getElementById("page-title").focus() and screen readers announce the new page.

On narrow screens the actions wrap below the title. render rejects more than one primary action.

Examples

List page with action

Data
render({
  "title": "Anmeldungen",
  "meta": "3 warten auf Freigabe · Formular online",
  "actions": [
    {
      "label": "Formular ansehen",
      "href": "/public/registration",
      "icon": "external-link"
    }
  ]
})
Markup
<header class="page-header">
<div class="page-header__text">
<h1 class="page-header__title" id="page-title" tabindex="-1">Anmeldungen</h1>
<p class="page-header__meta">3 warten auf Freigabe · Formular online</p>
</div>
<div class="button-row"><a class="button" href="/public/registration"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#external-link"/></svg>Formular ansehen</a></div>
</header>

Detail page with back link

Data
render({
  "title": "Lea Müller",
  "meta": "Kinder 7–10 · 6. Kup · seit 2024",
  "back": {
    "label": "Mitglieder",
    "href": "/admin/members"
  },
  "actions": [
    {
      "icon": "more",
      "iconOnly": true,
      "variant": "ghost",
      "ariaLabel": "Weitere Aktionen für Lea Müller"
    },
    {
      "label": "Bearbeiten",
      "variant": "primary",
      "icon": "edit"
    }
  ]
})
Markup
<header class="page-header">
<div class="page-header__text">
<a class="back-link" href="/admin/members"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#arrow-left"/></svg>Mitglieder</a>
<h1 class="page-header__title" id="page-title" tabindex="-1">Lea Müller</h1>
<p class="page-header__meta">Kinder 7–10 · 6. Kup · seit 2024</p>
</div>
<div class="button-row"><button class="button" type="button" data-variant="ghost" data-icon-only aria-label="Weitere Aktionen für Lea Müller"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#more"/></svg></button><button class="button" type="button" data-variant="primary"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#edit"/></svg>Bearbeiten</button></div>
</header>

Title only

Data
render({
  "title": "Einstellungen"
})
Markup
<header class="page-header">
<div class="page-header__text">
<h1 class="page-header__title" id="page-title" tabindex="-1">Einstellungen</h1>
</div>
</header>

Data contract

What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.

FieldTypeRequiredDefaultDescription
idstringoptional"page-title"Id of the h1 (focus target after navigation).
pattern ^[A-Za-z][\w-]*$
titlestringrequired—Page title — the same words as the nav item or the object name ("Anmeldungen", "Lea Müller").
min 1 chars
metastringoptional—One line of context or summary ("3 warten auf Freigabe · Formular online").
backobjectoptional—Link to the parent page, shown above the title.
back.labelstringrequired—Parent page name ("Mitglieder").
min 1 chars
back.hrefstringrequired—URL of the parent page.
actionsarray of objectsoptional—Page actions (Button data), primary last. At most one primary.
max 3 items
actions[].labelstringoptional—Visible text. Required unless iconOnly.
min 1 chars
actions[].variant"secondary" | "primary" | "ghost" | "danger" | "danger-ghost"optional"secondary"Visual weight / meaning.
actions[].size"sm" | "md" | "lg"optional"md"sm (36 px) only in dense desktop tables; lg (52 px) for the single main action of a screen.
actions[].iconstringoptional—Icon id from the sprite, shown before the label.
actions[].iconOnlybooleanoptionalfalseSquare button showing only icon. Requires ariaLabel.
actions[].ariaLabelstringoptional—Accessible name when the visible label is missing or ambiguous ("Weitere Aktionen für Lea Müller").
actions[].hrefstringoptional—Render an <a> that looks like a button (navigation, not actions).
actions[].type"button" | "submit" | "reset"optional"button"Button type inside forms.
actions[].valuestringoptional—Form value, e.g. cancel / confirm inside <form method="dialog">.
actions[].namestringoptional—Form field name when the button submits a value.
actions[].disabledbooleanoptionalfalseNot available now. Prefer explaining why nearby over silently disabling.
actions[].busybooleanoptionalfalseAction in progress (aria-busy).
actions[].formNoValidatebooleanoptionalfalseSubmit without native validation — use on "Abbrechen" inside <form method="dialog"> so required fields do not block closing.
actions[].block"always" | "mobile"optional—Full width always, or only below 768 px.
actions[].behaviorstringoptional—Behaviour hook (data-behavior), e.g. open-dialog, theme-toggle.
actions[].actionstringoptional—Screen action name (data-action). Declared in the actions of the screen with a handler and a risk class; the engine adds undo or confirm accordingly.
pattern ^[a-z][a-zA-Z0-9-]*$
actions[].actionIdstringoptional—Id of the thing the action applies to (data-id), when the button is not inside an element that has one.
pattern ^[A-Za-z0-9_-]+$
actions[].targetstringoptional—Id of the element a behaviour acts on (data-target), e.g. the dialog to open.
actions[].popoverTargetstringoptional—Id of a popover menu this button toggles (popovertarget).
JSON Schema
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "title"
  ],
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[A-Za-z][\\w-]*$",
      "default": "page-title",
      "description": "Id of the `h1` (focus target after navigation)."
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "description": "Page title — the same words as the nav item or the object name (\"Anmeldungen\", \"Lea Müller\")."
    },
    "meta": {
      "type": "string",
      "description": "One line of context or summary (\"3 warten auf Freigabe · Formular online\")."
    },
    "back": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "label",
        "href"
      ],
      "description": "Link to the parent page, shown above the title.",
      "properties": {
        "label": {
          "type": "string",
          "minLength": 1,
          "description": "Parent page name (\"Mitglieder\")."
        },
        "href": {
          "type": "string",
          "description": "URL of the parent page."
        }
      }
    },
    "actions": {
      "type": "array",
      "maxItems": 3,
      "description": "Page actions (Button data), primary last. At most one `primary`.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [],
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "description": "Visible text. Required unless `iconOnly`."
          },
          "variant": {
            "type": "string",
            "enum": [
              "secondary",
              "primary",
              "ghost",
              "danger",
              "danger-ghost"
            ],
            "default": "secondary",
            "description": "Visual weight / meaning."
          },
          "size": {
            "type": "string",
            "enum": [
              "sm",
              "md",
              "lg"
            ],
            "default": "md",
            "description": "`sm` (36 px) only in dense desktop tables; `lg` (52 px) for the single main action of a screen."
          },
          "icon": {
            "type": "string",
            "description": "Icon id from the sprite, shown before the label."
          },
          "iconOnly": {
            "type": "boolean",
            "default": false,
            "description": "Square button showing only `icon`. Requires `ariaLabel`."
          },
          "ariaLabel": {
            "type": "string",
            "description": "Accessible name when the visible label is missing or ambiguous (\"Weitere Aktionen für Lea Müller\")."
          },
          "href": {
            "type": "string",
            "description": "Render an `<a>` that looks like a button (navigation, not actions)."
          },
          "type": {
            "type": "string",
            "enum": [
              "button",
              "submit",
              "reset"
            ],
            "default": "button",
            "description": "Button type inside forms."
          },
          "value": {
            "type": "string",
            "description": "Form value, e.g. `cancel` / `confirm` inside `<form method=\"dialog\">`."
          },
          "name": {
            "type": "string",
            "description": "Form field name when the button submits a value."
          },
          "disabled": {
            "type": "boolean",
            "default": false,
            "description": "Not available now. Prefer explaining why nearby over silently disabling."
          },
          "busy": {
            "type": "boolean",
            "default": false,
            "description": "Action in progress (`aria-busy`)."
          },
          "formNoValidate": {
            "type": "boolean",
            "default": false,
            "description": "Submit without native validation — use on \"Abbrechen\" inside `<form method=\"dialog\">` so required fields do not block closing."
          },
          "block": {
            "type": "string",
            "enum": [
              "always",
              "mobile"
            ],
            "description": "Full width always, or only below 768 px."
          },
          "behavior": {
            "type": "string",
            "description": "Behaviour hook (`data-behavior`), e.g. `open-dialog`, `theme-toggle`."
          },
          "action": {
            "type": "string",
            "pattern": "^[a-z][a-zA-Z0-9-]*$",
            "description": "Screen action name (`data-action`). Declared in the `actions` of the screen with a handler and a risk class; the engine adds undo or confirm accordingly."
          },
          "actionId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]+$",
            "description": "Id of the thing the action applies to (`data-id`), when the button is not inside an element that has one."
          },
          "target": {
            "type": "string",
            "description": "Id of the element a behaviour acts on (`data-target`), e.g. the dialog to open."
          },
          "popoverTarget": {
            "type": "string",
            "description": "Id of a popover menu this button toggles (`popovertarget`)."
          }
        },
        "description": "One button."
      }
    }
  }
}

Markup & states

Root: header.page-header — usable without render() by writing the markup directly.

Parts

PartDescription
.page-header__textColumn with back link, title and meta.
.back-linkBack to the parent page (arrow-left, flips in RTL).
.page-header__titleThe h1 (tabindex="-1" as focus target).
.page-header__metaMuted context line.
.button-rowActions at the end side; wrap below on narrow screens.

Accessibility

  • Exactly one h1 per page — the page header's title.
  • The h1 is focusable by script (tabindex="-1") so focus can move to it after a route change.
  • The back link names its destination; its arrow icon is decorative.

Guidance

DoUse the same title as the nav item and the document title.
DoPut rarely used actions in an overflow menu.
Don'tDon't put more than one primary action in a header.
Don'tDon't write "Zurück" — name the parent page.
Don'tDon't repeat the title in the meta line.

Related: App shell · Button · Tabs