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>
)
}<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>Component API
| Prop | Default | Description |
|---|---|---|
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. |
| Prop | Default | Description |
|---|---|---|
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
SandboxCreate 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>