Skip to main content
Urbicon UI
Back to Recipes

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.

Built with Button Breadcrumb Tab

Live preview

ListPageHeader.svelte

Narrow the window below 640px and the action row drops below the title.

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

The 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

Click through the tabs: the strip belongs to the header, the panels it switches do not.

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.