Responsive app page header with back link, badges and actions that fold into an overflow menu instead of wrapping
Tags: component layout header actions responsive toolbar
ActionHeader is the page and record header for dashboard-style apps. Its main feature is that the action row never wraps. As the header gets narrower, secondary actions move into a "More actions" (⋯) menu, so the title and the primary action stay on one line.
Layout responds to the header's own width through container queries, not the viewport width. It therefore adapts when a sidebar opens or collapses, and consumers don't need a Tailwind container-query plugin.
Use PageHeader for marketing and landing-page titles. Use ActionHeader for app pages.
Import
<script>
import { ActionHeader, type ActionHeaderAction } from '@classic-homes/theme-svelte';
</script>Basic Usage
<ActionHeader
title="Signs"
subtitle="Physical signage inventory across all communities."
actions={[
{ label: 'Export CSV', icon: 'download', onSelect: exportCsv },
{ label: 'Add sign', icon: 'plus', priority: 'primary', onSelect: openCreate },
]}
/>Layout
wide (≥ 40rem)
← Back to signs
[media] Title ························ [secondary…][Primary][⋯] [tools]
Subtitle
[badges]
[toolbar]
narrow (< 40rem)
← Back to signs
Title (2-line clamp) ··········· [Primary][⋯]
Subtitle (2-line clamp)
[badges] ························ [tools]
[toolbar]At 20rem (320px) or narrower, a primary action that has an icon hides its label visually. The label stays as the accessible name.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string |
required | Page or record title (clamped to two lines) |
subtitle |
string |
- | Description under the title |
headingLevel |
1 | 2 | 3 |
1 |
Heading element for the title |
caption |
string |
- | Small note under the actions, right-aligned (e.g. "Updated 5 minutes ago") |
backHref |
string |
- | Shows a labelled BackButton above the title, linking here |
backLabel |
string |
- | Back target name, shown as "Back to {backLabel}" |
actions |
ActionHeaderAction[] |
[] |
Header actions (see below) |
collapseAt |
'auto' | 'sm' | 'md' | 'lg' | 'never' |
'auto' |
Header width at which secondary actions fold: sm ≈ 28rem, md ≈ 40rem, lg ≈ 52rem. auto means lg with 2+ secondary actions, otherwise md |
moreLabel |
string |
'More actions' |
Accessible name of the ⋯ trigger |
media |
Snippet |
- | Leading visual beside the title, e.g. a thumbnail |
titleSuffix |
Snippet |
- | Small inline content right after the title |
badges |
Snippet |
- | Status badges or a meta line under the title |
tools |
Snippet |
- | Icon buttons that are always visible at the end of the header |
toolbar |
Snippet |
- | Row of controls under the header (view toggles, selects) |
class |
string |
- | Additional classes |
ActionHeaderAction
| Field | Type | Description |
|---|---|---|
label |
string |
Button or menu item text |
icon |
IconName |
Leading icon |
href |
string |
Render the action as a link |
onSelect |
() => void |
Click or select handler |
priority |
'primary' | 'secondary' | 'overflow' |
Default secondary. See the rules below |
variant |
Button variant | Inline button variant. Default: default for primary, outline otherwise |
disabled |
boolean |
|
title |
string |
Tooltip on the inline button, e.g. why the action is disabled |
loading |
boolean |
Shows a spinner inline; disables the item in the menu |
destructive |
boolean |
Destructive menu styling; sorted last, after a separator |
hidden |
boolean |
Leaves the action out, so arrays can include permission conditionals inline |
Priority rules:
primary: always a visible button. Use at most one.secondary: an inline outline button on wide headers. It moves into the ⋯ menu belowcollapseAt.overflow: always in the ⋯ menu. Use it for rare or destructive actions.
Examples
Record detail
<ActionHeader
title={sign.name}
subtitle={sign.community}
backHref="/marketing/signs"
backLabel="Signs"
actions={[
{ label: 'Check in', icon: 'check', priority: 'primary', onSelect: checkIn },
{ label: 'Edit', icon: 'edit', onSelect: edit },
{ label: 'Retire', priority: 'overflow', destructive: true, onSelect: retire },
]}
>
{#snippet badges()}
<Badge>Installed</Badge>
{/snippet}
{#snippet tools()}
<RecordTools />
{/snippet}
</ActionHeader>With a toolbar
<script>
let groupBy = $state('community');
let view = $state('list');
</script>
<ActionHeader
title="All Available Homes"
subtitle="89 quick move-in homes across all communities"
caption="Updated 5 minutes ago"
actions={[{ label: 'PDF', icon: 'file-text', priority: 'primary', variant: 'secondary' }]}
>
{#snippet toolbar()}
<Toolbar label="Inventory tools" class="w-full">
<Input placeholder="Search homes…" class="max-w-sm" />
<Button variant="secondary" size="sm">Filters</Button>
<span class="text-sm">89 homes</span>
{#snippet end()}
<SegmentedControl
label="Group homes by"
size="sm"
bind:value={groupBy}
options={[
{ value: 'community', label: 'Community' },
{ value: 'collection', label: 'Collection' },
{ value: 'floor-plan', label: 'Floor Plan' },
]}
/>
<ViewToggle bind:value={view} />
{/snippet}
</Toolbar>
{/snippet}
</ActionHeader>Accessibility
- The title is a real heading (
h1by default). UseheadingLevelfor nested regions. - The title takes the theme's type style for its level:
h1is Spectral 500 italic (2.5rem wide, 1.75rem narrow);h2andh3are Figtree 600 (1.75rem / 1.5rem wide). Section headers such as a community name should useheadingLevel={2}. - The back button's aria-label is "Back to {backLabel}".
- The ⋯ trigger is a single
<button>with an accessible name (moreLabel). - A primary action that shows only its icon still has its label as visually hidden text.
Notes
- Svelte only. There is no React equivalent yet.