Detail header
The top of a detail page: the name or the amount, its badges, the identifier to copy, a date, and the actions.
Awa Diallo
ActiveBusinessimport { Avatar } from '@/components/avatar'
import { Badge } from '@/components/badge'
import { Button } from '@/components/button'
import { DetailHeader } from '@/components/detail-header'
import { LockClosedIcon, PencilSquareIcon } from '@heroicons/react/16/solid'
export default function CustomerPage() {
return (
<DetailHeader
title="Awa Diallo"
avatar={<Avatar initials="AD" className="size-12 shrink-0 bg-zinc-900 text-white sm:size-11 dark:bg-white dark:text-zinc-900" />}
badges={
<>
<Badge color="green">Active</Badge>
<Badge color="blue">Business</Badge>
</>
}
id="cus_91f2a4c1e"
idLabel="Copy customer ID"
actions={
<>
<Button outline className="max-sm:flex-1">
<PencilSquareIcon data-slot="icon" />
Edit
</Button>
<Button outline className="max-sm:flex-1">
<LockClosedIcon data-slot="icon" />
Block
</Button>
</>
}
/>
)
}<script setup lang="ts">
import Avatar from '@/components/avatar.vue'
import Badge from '@/components/badge.vue'
import Button from '@/components/button.vue'
import DetailHeader from '@/components/detail-header.vue'
import { LockClosedIcon, PencilSquareIcon } from '@heroicons/vue/16/solid'
</script>
<template>
<DetailHeader title="Awa Diallo" id="cus_91f2a4c1e" id-label="Copy customer ID">
<template #avatar>
<Avatar initials="AD" class="size-12 shrink-0 bg-zinc-900 text-white sm:size-11 dark:bg-white dark:text-zinc-900" />
</template>
<template #badges>
<Badge color="green">Active</Badge>
<Badge color="blue">Business</Badge>
</template>
<template #actions>
<Button outline class="max-sm:flex-1">
<PencilSquareIcon data-slot="icon" />
Edit
</Button>
<Button outline class="max-sm:flex-1">
<LockClosedIcon data-slot="icon" />
Block
</Button>
</template>
</DetailHeader>
</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 { CopyableId } from './copyable-id'
export function DetailHeader({
title,
size = 'md',
placeholder = false,
avatar,
badges,
id,
idLabel = 'Copy ID',
meta,
actions,
className,
...props
}: {
title: React.ReactNode
size?: 'md' | 'lg'
placeholder?: boolean
avatar?: React.ReactNode
badges?: React.ReactNode
id?: string
idLabel?: string
meta?: React.ReactNode
actions?: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<'div'>, 'title'>) {
return (
<div {...props} className={clsx(className, 'flex flex-wrap items-start justify-between gap-x-6 gap-y-4')}>
<div className="flex min-w-0 items-center gap-3 sm:gap-4">
{avatar}
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
<h1
className={clsx(
'min-w-0 truncate font-semibold tracking-tight',
size === 'lg' ? 'text-3xl/9 tabular-nums sm:text-2xl/8' : 'text-2xl/8 sm:text-xl/8',
placeholder ? 'text-zinc-500 italic dark:text-zinc-400' : 'text-zinc-950 dark:text-white'
)}
>
{title}
</h1>
{badges}
</div>
{id || meta ? (
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5">
{id ? <CopyableId value={id} label={idLabel} /> : null}
{id && meta ? (
<span aria-hidden="true" className="text-zinc-300 max-sm:hidden dark:text-zinc-600">
·
</span>
) : null}
{meta ? (
<span className="text-base/6 sm:whitespace-nowrap text-zinc-500 sm:text-sm/6 dark:text-zinc-400">{meta}</span>
) : null}
</div>
) : null}
</div>
</div>
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2 max-sm:w-full">{actions}</div> : null}
</div>
)
}Also copy Copy button.
<script setup lang="ts">
import CopyableId from './copyable-id.vue'
withDefaults(
defineProps<{
title: string
size?: 'md' | 'lg'
placeholder?: boolean
id?: string
idLabel?: string
}>(),
{ size: 'md', placeholder: false, id: undefined, idLabel: 'Copy ID' }
)
</script>
<template>
<div class="flex flex-wrap items-start justify-between gap-x-6 gap-y-4">
<div class="flex min-w-0 items-center gap-3 sm:gap-4">
<slot name="avatar" />
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1.5">
<h1
:class="[
'min-w-0 truncate font-semibold tracking-tight',
size === 'lg' ? 'text-3xl/9 tabular-nums sm:text-2xl/8' : 'text-2xl/8 sm:text-xl/8',
placeholder ? 'text-zinc-500 italic dark:text-zinc-400' : 'text-zinc-950 dark:text-white',
]"
>
{{ title }}
</h1>
<slot name="badges" />
</div>
<div v-if="id || $slots.meta" class="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5">
<CopyableId v-if="id" :value="id" :label="idLabel" />
<span v-if="id && $slots.meta" aria-hidden="true" class="text-zinc-300 max-sm:hidden dark:text-zinc-600">·</span>
<span v-if="$slots.meta" class="text-base/6 sm:whitespace-nowrap text-zinc-500 sm:text-sm/6 dark:text-zinc-400">
<slot name="meta" />
</span>
</div>
</div>
</div>
<div v-if="$slots.actions" class="flex shrink-0 flex-wrap items-center gap-2 max-sm:w-full">
<slot name="actions" />
</div>
</div>
</template>Also copy Copy button.
Component API
| Prop | Default | Description |
|---|---|---|
DetailHeaderrenders a <div> element | ||
title | - | The name of the record, or its amount. |
size | md | `lg` for an amount as the title, with tabular figures. |
placeholder | false | The title stands in for a missing name: muted and italic. |
avatar | - | An Avatar before the title. |
badges | - | Beside the title: status, type, flags. |
id | - | Identifier shown under the title with a copy button. |
idLabel | Copy ID | Accessible label of that copy button. |
meta | - | After the identifier: a date, a count. |
actions | - | On the right. Under `sm` they take the full width: give each button `max-sm:flex-1`. |
CopyableIdrenders a <span> element | ||
value | - | The identifier. |
label | Copy | Accessible label of the copy button: "Copy payment ID". |
tone | muted | `muted` under a title, `strong` among facts. |
boxed | false | On a tinted well, for a long URL. |
| Prop | Default | Description |
|---|---|---|
DetailHeaderrenders a <div> element | ||
title | - | The name of the record, or its amount. |
size | md | `lg` for an amount as the title, with tabular figures. |
placeholder | false | The title stands in for a missing name: muted and italic. |
#avatar | - | An Avatar before the title. |
#badges | - | Beside the title: status, type, flags. |
id | - | Identifier shown under the title with a copy button. |
idLabel | Copy ID | Accessible label of that copy button. |
#meta | - | After the identifier: a date, a count. |
#actions | - | On the right. Under `sm` they take the full width: give each button `max-sm:flex-1`. |
CopyableIdrenders a <span> element | ||
value | - | The identifier. |
label | Copy | Accessible label of the copy button: "Copy payment ID". |
tone | muted | `muted` under a title, `strong` among facts. |
boxed | false | On a tinted well, for a long URL. |
Examples
Basic example
A record with a name: an avatar, the name, its badges, and the identifier under it. Actions on the right, each with max-sm:flex-1 so they share the row on a phone:
Awa Diallo
ActiveBusinessimport { Avatar } from '@/components/avatar'
import { Badge } from '@/components/badge'
import { Button } from '@/components/button'
import { DetailHeader } from '@/components/detail-header'
import { LockClosedIcon, PencilSquareIcon } from '@heroicons/react/16/solid'
export default function CustomerPage() {
return (
<DetailHeader
title="Awa Diallo"
avatar={<Avatar initials="AD" className="size-12 shrink-0 bg-zinc-900 text-white sm:size-11 dark:bg-white dark:text-zinc-900" />}
badges={
<>
<Badge color="green">Active</Badge>
<Badge color="blue">Business</Badge>
</>
}
id="cus_91f2a4c1e"
idLabel="Copy customer ID"
actions={
<>
<Button outline className="max-sm:flex-1">
<PencilSquareIcon data-slot="icon" />
Edit
</Button>
<Button outline className="max-sm:flex-1">
<LockClosedIcon data-slot="icon" />
Block
</Button>
</>
}
/>
)
}<script setup lang="ts">
import Avatar from '@/components/avatar.vue'
import Badge from '@/components/badge.vue'
import Button from '@/components/button.vue'
import DetailHeader from '@/components/detail-header.vue'
import { LockClosedIcon, PencilSquareIcon } from '@heroicons/vue/16/solid'
</script>
<template>
<DetailHeader title="Awa Diallo" id="cus_91f2a4c1e" id-label="Copy customer ID">
<template #avatar>
<Avatar initials="AD" class="size-12 shrink-0 bg-zinc-900 text-white sm:size-11 dark:bg-white dark:text-zinc-900" />
</template>
<template #badges>
<Badge color="green">Active</Badge>
<Badge color="blue">Business</Badge>
</template>
<template #actions>
<Button outline class="max-sm:flex-1">
<PencilSquareIcon data-slot="icon" />
Edit
</Button>
<Button outline class="max-sm:flex-1">
<LockClosedIcon data-slot="icon" />
Block
</Button>
</template>
</DetailHeader>
</template>An amount as the title
For a payment or a refund, the amount is the title: size="lg" sets it bigger with tabular figures. meta adds the date after the identifier, separated by a dot that disappears when the row wraps:
XOF 12,500
Succeeded<DetailHeader
size="lg"
title="XOF 12,500"
badges={<Badge color="green">Succeeded</Badge>}
id="pay_8f2a4c1e"
idLabel="Copy payment ID"
meta="Sep 10, 2026, 9:14 AM"
actions={
<>
<Button outline className="max-sm:flex-1">
<DocumentTextIcon data-slot="icon" />
Receipt
</Button>
<Button outline className="max-sm:flex-1">
<ArrowUturnLeftIcon data-slot="icon" />
Refund
</Button>
</>
}
/><DetailHeader size="lg" title="XOF 12,500" id="pay_8f2a4c1e" id-label="Copy payment ID">
<template #badges>
<Badge color="green">Succeeded</Badge>
</template>
<template #meta>Sep 10, 2026, 9:14 AM</template>
<template #actions>
<Button outline class="max-sm:flex-1">
<DocumentTextIcon data-slot="icon" />
Receipt
</Button>
<Button outline class="max-sm:flex-1">
<ArrowUturnLeftIcon data-slot="icon" />
Refund
</Button>
</template>
</DetailHeader>Placeholder title
When the record has no name yet, say so and pass placeholder: the title turns muted and italic instead of pretending:
Unnamed customer
<DetailHeader title="Unnamed customer" placeholder id="cus_02b7e" idLabel="Copy customer ID" /><DetailHeader title="Unnamed customer" placeholder id="cus_02b7e" id-label="Copy customer ID" />With breadcrumbs
Put the Breadcrumbs above and give the header mt-4:
XOF 12,500
Succeeded<Breadcrumbs>
<BreadcrumbItem href="/payments">Payments</BreadcrumbItem>
<BreadcrumbItem>pay_8f2a4c1e</BreadcrumbItem>
</Breadcrumbs>
<DetailHeader className="mt-4" size="lg" title="XOF 12,500" … /><Breadcrumbs>
<BreadcrumbItem href="/payments">Payments</BreadcrumbItem>
<BreadcrumbItem>pay_8f2a4c1e</BreadcrumbItem>
</Breadcrumbs>
<DetailHeader class="mt-4" size="lg" title="XOF 12,500" … />Copyable ID on its own
The identifier line is its own component. muted under a title, strong among facts, boxed for a long URL on a well:
<CopyableId value="pay_8f2a4c1e" label="Copy payment ID" />
<CopyableId value="pay_8f2a4c1e" label="Copy payment ID" tone="strong" />
<CopyableId value="https://api.example.com/v1/payments/pay_8f2a4c1e" label="Copy URL" boxed />