Skip to content
  1. Page structure
  2. Page header

Page header

The top of a list page: a title, a line under it, and the actions on the right.

Customers

Everyone who has paid you, or is about to.

import { Button } from '@/components/button'
import { PageHeader } from '@/components/page-header'
import { ArrowDownTrayIcon, UserPlusIcon } from '@heroicons/react/16/solid'

export default function CustomersPage() {
  return (
    <PageHeader
      title="Customers"
      description="Everyone who has paid you, or is about to."
      actions={
        <>
          <Button outline>
            <ArrowDownTrayIcon data-slot="icon" />
            Export
          </Button>
          <Button href="/customers/new">
            <UserPlusIcon data-slot="icon" />
            Add customer
          </Button>
        </>
      }
    />
  )
}
<script setup lang="ts">
import Button from '@/components/button.vue'
import PageHeader from '@/components/page-header.vue'
import { ArrowDownTrayIcon, UserPlusIcon } from '@heroicons/vue/16/solid'
</script>

<template>
  <PageHeader title="Customers" description="Everyone who has paid you, or is about to.">
    <template #actions>
      <Button outline>
        <ArrowDownTrayIcon data-slot="icon" />
        Export
      </Button>
      <Button href="/customers/new">
        <UserPlusIcon data-slot="icon" />
        Add customer
      </Button>
    </template>
  </PageHeader>
</template>

Installation

Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.

import clsx from 'clsx'
import type React from 'react'
import { Heading } from './heading'
import { Text } from './text'

export function PageHeader({
  title,
  description,
  badge,
  actions,
  className,
  ...props
}: {
  title: React.ReactNode
  description?: React.ReactNode
  badge?: React.ReactNode
  actions?: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<'div'>, 'title'>) {
  return (
    <div {...props} className={clsx(className, 'flex flex-wrap items-end justify-between gap-4')}>
      <div className="min-w-0">
        {badge ? (
          <div className="flex flex-wrap items-center gap-x-2.5 gap-y-1">
            <Heading>{title}</Heading>
            {badge}
          </div>
        ) : (
          <Heading>{title}</Heading>
        )}
        {description ? <Text className="mt-1">{description}</Text> : null}
      </div>
      {actions ? <div className="flex max-w-full shrink-0 flex-wrap items-center gap-3">{actions}</div> : null}
    </div>
  )
}

Also copy Heading, Text.

<script setup lang="ts">
import Heading from './heading.vue'
import Text from './text.vue'

defineProps<{ title: string; description?: string }>()
</script>

<template>
  <div class="flex flex-wrap items-end justify-between gap-4">
    <div class="min-w-0">
      <div v-if="$slots.badge" class="flex flex-wrap items-center gap-x-2.5 gap-y-1">
        <Heading>{{ title }}</Heading>
        <slot name="badge" />
      </div>
      <Heading v-else>{{ title }}</Heading>
      <Text v-if="description" class="mt-1">{{ description }}</Text>
    </div>
    <div v-if="$slots.actions" class="flex max-w-full shrink-0 flex-wrap items-center gap-3">
      <slot name="actions" />
    </div>
  </div>
</template>

Also copy Heading, Text.

Component API

PropDefaultDescription
PageHeaderrenders a <div> element
title-The Heading of the page.
description-One line under the title.
badge-Beside the title: the environment, a beta mark.
actions-On the right, aligned with the description: buttons, a menu, a switch.
PropDefaultDescription
PageHeaderrenders a <div> element
title-The Heading of the page.
description-One line under the title.
#badge-Beside the title: the environment, a beta mark.
#actions-On the right, aligned with the description: buttons, a menu, a switch.

Examples

Basic example

Title and description on the left, actions on the right. The actions sit on the baseline of the description and wrap under it when the row is too narrow:

Customers

Everyone who has paid you, or is about to.

import { Button } from '@/components/button'
import { PageHeader } from '@/components/page-header'
import { ArrowDownTrayIcon, UserPlusIcon } from '@heroicons/react/16/solid'

export default function CustomersPage() {
  return (
    <PageHeader
      title="Customers"
      description="Everyone who has paid you, or is about to."
      actions={
        <>
          <Button outline>
            <ArrowDownTrayIcon data-slot="icon" />
            Export
          </Button>
          <Button href="/customers/new">
            <UserPlusIcon data-slot="icon" />
            Add customer
          </Button>
        </>
      }
    />
  )
}
<script setup lang="ts">
import Button from '@/components/button.vue'
import PageHeader from '@/components/page-header.vue'
import { ArrowDownTrayIcon, UserPlusIcon } from '@heroicons/vue/16/solid'
</script>

<template>
  <PageHeader title="Customers" description="Everyone who has paid you, or is about to.">
    <template #actions>
      <Button outline>
        <ArrowDownTrayIcon data-slot="icon" />
        Export
      </Button>
      <Button href="/customers/new">
        <UserPlusIcon data-slot="icon" />
        Add customer
      </Button>
    </template>
  </PageHeader>
</template>

Without actions

Most settings pages only need the title and the line:

Preferences

Language, currency and what lands in your inbox.

<PageHeader title="Preferences" description="Language, currency and what lands in your inbox." />

With a badge

A badge sits beside the title, for the environment or a beta mark:

API keys

Sandbox

Create and manage API keys for your integrations. You are viewing sandbox keys.

<PageHeader
  title="API keys"
  description="Create and manage API keys for your integrations. You are viewing sandbox keys."
  badge={<Badge color="amber">Sandbox</Badge>}
/>
<PageHeader
  title="API keys"
  description="Create and manage API keys for your integrations. You are viewing sandbox keys."
>
  <template #badge>
    <Badge color="amber">Sandbox</Badge>
  </template>
</PageHeader>

Spacing below

The header sets no bottom margin. The block that follows brings its own: mt-6 for filters or tabs, mt-8 for a table or an empty state, mt-10 for settings sections:

Payments

Track and manage your customer transactions.

<PageHeader title="Payments" description="Track and manage your customer transactions." />
<FilterBar className="mt-6">…</FilterBar>
<PageHeader title="Payments" description="Track and manage your customer transactions." />
<FilterBar class="mt-6">…</FilterBar>