Skip to content
  1. Page structure
  2. Detail header

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

ActiveBusiness
cus_91f2a4c1e
import { 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

PropDefaultDescription
DetailHeaderrenders a <div> element
title-The name of the record, or its amount.
sizemd`lg` for an amount as the title, with tabular figures.
placeholderfalseThe 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.
idLabelCopy IDAccessible 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.
labelCopyAccessible label of the copy button: "Copy payment ID".
tonemuted`muted` under a title, `strong` among facts.
boxedfalseOn a tinted well, for a long URL.
PropDefaultDescription
DetailHeaderrenders a <div> element
title-The name of the record, or its amount.
sizemd`lg` for an amount as the title, with tabular figures.
placeholderfalseThe 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.
idLabelCopy IDAccessible 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.
labelCopyAccessible label of the copy button: "Copy payment ID".
tonemuted`muted` under a title, `strong` among facts.
boxedfalseOn 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

ActiveBusiness
cus_91f2a4c1e
import { 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
pay_8f2a4c1eSep 10, 2026, 9:14 AM
<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

cus_02b7e
<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
pay_8f2a4c1eSep 10, 2026, 9:14 AM
<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:

pay_8f2a4c1epay_8f2a4c1ehttps://api.example.com/v1/payments/pay_8f2a4c1e
<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 />