Page Header
A top-of-page header in plain markup: eyebrow or breadcrumb, title, subtitle, and an action row that stacks below them on narrow screens. Three variants: list page, detail page with a Breadcrumb, tabbed page with a Tab strip.
Live preview
ListPageHeader.svelte
Property management
Apartments
Master data of all managed apartments: addresses, occupancy, last inspection.
<script lang="ts">
import { Button } from '@urbicon-ui/blocks';
</script>
<!-- The top of your page's content column; the page owns the gap below it. -->
<header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
<div class="min-w-0 flex-1">
<p class="text-text-tertiary mb-1 text-xs font-medium tracking-wide uppercase">
Property management
</p>
<h1 class="text-text-primary text-3xl font-semibold tracking-tight">Apartments</h1>
<!-- The subtitle narrows the scope; drop the <p> when it would only repeat the title. -->
<p class="text-text-secondary mt-1.5 max-w-2xl text-sm leading-relaxed sm:text-base">
Master data of all managed apartments: addresses, occupancy, last inspection.
</p>
</div>
<div class="flex flex-wrap items-center gap-2 sm:shrink-0">
<Button intent="primary">New apartment</Button>
</div>
</header>DetailPageHeader.svelte
Breadcrumb sits in the eyebrow's place, and a ghost Archive button joins the primary action.Sunset Heights
18 apartments · 4 floors · built 1987.
<script lang="ts">
import { Breadcrumb, Button } from '@urbicon-ui/blocks';
</script>
<header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
<div class="min-w-0 flex-1">
<div class="mb-2">
<Breadcrumb items={[{ label: 'Buildings', href: '#' }, { label: 'Sunset Heights' }]} />
</div>
<h1 class="text-text-primary text-3xl font-semibold tracking-tight">Sunset Heights</h1>
<p class="text-text-secondary mt-1.5 max-w-2xl text-sm leading-relaxed sm:text-base">
18 apartments · 4 floors · built 1987.
</p>
</div>
<!-- flex-wrap lets the action pair break to a second line instead of squeezing the title. -->
<div class="flex flex-wrap items-center gap-2 sm:shrink-0">
<Button variant="ghost" intent="neutral">Archive</Button>
<Button intent="primary">Edit</Button>
</div>
</header>TabPageHeader.svelte
Settings
Manage your account, billing, and team preferences.
<script lang="ts">
import { Button, Tab, TabItem } from '@urbicon-ui/blocks';
let activeTab = $state('overview');
</script>
<!-- The same row as the list header, wrapped so the Tab strip joins it inside <header>. -->
<header class="flex flex-col gap-4">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
<div class="min-w-0 flex-1">
<h1 class="text-text-primary text-3xl font-semibold tracking-tight">Settings</h1>
<p class="text-text-secondary mt-1.5 max-w-2xl text-sm leading-relaxed sm:text-base">
Manage your account, billing, and team preferences.
</p>
</div>
<div class="flex flex-wrap items-center gap-2 sm:shrink-0">
<Button variant="outlined" intent="neutral">Export</Button>
</div>
</div>
<!-- The strip is part of the header; the panels it switches are the page body below. -->
<Tab bind:value={activeTab} variant="line">
{#snippet tabs()}
<TabItem value="overview">Overview</TabItem>
<TabItem value="billing">Billing</TabItem>
<TabItem value="team">Team</TabItem>
{/snippet}
</Tab>
</header>Three decisions
One h1 per page
The snippets write <h1> because this header
is usually the page's top heading. Inside a Dialog or Drawer, or on a route whose layout already renders
the h1, drop to <h2> and take the type a step
down (text-2xl). The demos above render h2 for the
same reason: this docs page brings its own h1.
Eyebrow or breadcrumb, not both
Both sit in the slot above the title. The eyebrow is a category label ("Property
management"): context that does not navigate. The Breadcrumb is a trail of routes you can step back through.
The list header carries the eyebrow, the detail header the trail; stacked, two lines would name
the same context twice. Reach for the breadcrumb only when every ancestor is a real page.
Why this stays markup
Nothing here computes: the header renders what the page already knows, and its one piece
of state, the tab value, is page state it merely displays. A wrapper component would hide
the markup without saving any logic, and it would fix the structure at the spots where
pages differ most: a status Badge beside the title,
a filter Input in the action row. Copy the variant that
matches your page and edit it in place.