Skip to content
  1. Layouts
  2. Detail layout

Detail layout

The body of a detail page: the record on the left, its summary on the right in a rail that stays in view. Under lg the rail follows the content. The header above it is the Detail header.

import { BellAlertIcon, CheckIcon, DocumentTextIcon, EyeIcon, PaperAirplaneIcon, XCircleIcon } from '@heroicons/react/16/solid'
import { Avatar } from '@/components/avatar'
import { Badge } from '@/components/badge'
import { BreadcrumbItem, Breadcrumbs } from '@/components/breadcrumbs'
import { Button } from '@/components/button'
import { DetailHeader } from '@/components/detail-header'
import { DetailLayout, DetailRail, DetailRailSection } from '@/components/detail-layout'
import { Divider } from '@/components/divider'
import { FactList } from '@/components/fact-list'
import { NoteList } from '@/components/notes'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/table'
import { TextLink } from '@/components/text'
import { Timeline, TimelineItem } from '@/components/timeline'

const lines = [
  { item: 'Scale plan, 12 seats', quantity: 12, price: '$200.00', total: '$2,400.00' },
  { item: 'Priority support, yearly', quantity: 1, price: '$600.00', total: '$600.00' },
  { item: 'Onboarding session', quantity: 2, price: '$250.00', total: '$500.00' },
]

const totals = [
  { label: 'Subtotal', value: '$3,500.00' },
  { label: 'VAT (20%)', value: '$700.00' },
  { label: 'Paid', value: '$0.00' },
]

const notes = [
  {
    id: 'n1',
    body: 'Finance at Lumen Health moves to a new card on October 10. Hold the next retry until then.',
    author: 'Awa Diallo',
    date: 'Oct 3, 2026',
    pinned: true,
  },
  { id: 'n2', body: 'PO number requested for their records, sent by email.', author: 'Kofi Mensah', date: 'Sep 19, 2026' },
]

const sectionTitle = 'text-lg/7 font-semibold text-zinc-950 sm:text-base/6 dark:text-white'

export default function Invoice() {
  return (
    <>
      <Breadcrumbs>
        <BreadcrumbItem href="/invoices">Invoices</BreadcrumbItem>
        <BreadcrumbItem>INV-2041</BreadcrumbItem>
      </Breadcrumbs>
      <DetailHeader
        className="mt-4"
        size="lg"
        title="$4,200.00"
        badges={<Badge color="red">Overdue</Badge>}
        id="INV-2041"
        idLabel="Copy invoice number"
        meta="Due Oct 2, 2026"
        actions={
          <>
            <Button outline className="max-sm:flex-1">
              <BellAlertIcon />
              Send reminder
            </Button>
            <Button className="max-sm:flex-1">
              <CheckIcon />
              Record payment
            </Button>
          </>
        }
      />

      <DetailLayout
        className="mt-10"
        asideLabel="Invoice summary"
        aside={
          <DetailRail>
            <DetailRailSection label="Billed to">
              <div className="flex min-w-0 items-center gap-3">
                <Avatar src="/avatars/arthur.jpg" className="size-10 shrink-0" />
                <div className="min-w-0 text-base/6 sm:text-sm/6">
                  <p className="truncate font-medium text-zinc-950 dark:text-white">Arthur Monney</p>
                  <p className="truncate text-zinc-500 dark:text-zinc-400">Lumen Health</p>
                </div>
              </div>
              <p className="mt-3 text-base/6 sm:text-sm/6">
                <TextLink href="/customers/cus_8Hq2Lm">View customer</TextLink>
              </p>
            </DetailRailSection>
            <DetailRailSection label="Payment">
              <FactList
                layout="rows"
                facts={[
                  { label: 'Method', value: 'Mastercard •••• 5100' },
                  { label: 'Terms', value: 'Net 14' },
                  { label: 'Issued', value: 'Sep 18, 2026' },
                  { label: 'Due', value: 'Oct 2, 2026' },
                ]}
              />
            </DetailRailSection>
            <DetailRailSection label="Details">
              <FactList
                layout="rows"
                facts={[
                  { label: 'Currency', value: 'USD' },
                  { label: 'PO number', value: undefined },
                  { label: 'Language', value: 'English' },
                ]}
              />
            </DetailRailSection>
          </DetailRail>
        }
      >
        <section aria-labelledby="line-items">
          <h2 id="line-items" className={sectionTitle}>
            Line items
          </h2>
          <Table dense className="mt-4">
            <TableHead>
              <TableRow>
                <TableHeader>Item</TableHeader>
                <TableHeader className="text-right">Qty</TableHeader>
                <TableHeader className="text-right">Unit price</TableHeader>
                <TableHeader className="text-right">Amount</TableHeader>
              </TableRow>
            </TableHead>
            <TableBody>
              {lines.map((line) => (
                <TableRow key={line.item}>
                  <TableCell className="font-medium">{line.item}</TableCell>
                  <TableCell className="text-right tabular-nums">{line.quantity}</TableCell>
                  <TableCell className="text-right tabular-nums">{line.price}</TableCell>
                  <TableCell className="text-right tabular-nums">{line.total}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
          <dl className="mt-6 ml-auto max-w-xs text-base/6 sm:text-sm/6">
            {totals.map((row) => (
              <div key={row.label} className="flex justify-between gap-4 py-1.5">
                <dt className="text-zinc-500 dark:text-zinc-400">{row.label}</dt>
                <dd className="text-zinc-950 tabular-nums dark:text-white">{row.value}</dd>
              </div>
            ))}
            <div className="mt-2 flex justify-between gap-4 border-t border-zinc-950/10 pt-3 dark:border-white/10">
              <dt className="font-semibold text-zinc-950 dark:text-white">Amount due</dt>
              <dd className="font-semibold text-zinc-950 tabular-nums dark:text-white">$4,200.00</dd>
            </div>
          </dl>
        </section>

        <Divider soft className="my-10" />

        <section aria-labelledby="activity">
          <h2 id="activity" className={sectionTitle}>
            Activity
          </h2>
          <Timeline className="mt-6">
            <TimelineItem icon={BellAlertIcon} color="amber" title="Reminder sent to arthur@lumenhealth.io" time="Oct 5" dateTime="2026-10-05" />
            <TimelineItem
              icon={XCircleIcon}
              color="red"
              title="Automatic payment failed"
              description="Mastercard •••• 5100 was declined for insufficient funds."
              time="Oct 2"
              dateTime="2026-10-02"
            />
            <TimelineItem icon={EyeIcon} title="Viewed by the customer" time="Sep 20" dateTime="2026-09-20" />
            <TimelineItem icon={PaperAirplaneIcon} color="sky" title="Sent to arthur@lumenhealth.io" time="Sep 18" dateTime="2026-09-18" />
            <TimelineItem icon={DocumentTextIcon} title="Created by Awa Diallo" time="Sep 18" dateTime="2026-09-18" />
          </Timeline>
        </section>

        <Divider soft className="my-10" />

        <section aria-labelledby="notes">
          <h2 id="notes" className={sectionTitle}>
            Notes
          </h2>
          <NoteList className="mt-4" notes={notes} />
        </section>
      </DetailLayout>
    </>
  )
}
<script setup lang="ts">
import { BellAlertIcon, CheckIcon, DocumentTextIcon, EyeIcon, PaperAirplaneIcon, XCircleIcon } from '@heroicons/vue/16/solid'
import Avatar from '@/components/avatar.vue'
import Badge from '@/components/badge.vue'
import BreadcrumbItem from '@/components/breadcrumb-item.vue'
import Breadcrumbs from '@/components/breadcrumbs.vue'
import Button from '@/components/button.vue'
import DetailHeader from '@/components/detail-header.vue'
import DetailLayout from '@/components/detail-layout.vue'
import DetailRail from '@/components/detail-rail.vue'
import DetailRailSection from '@/components/detail-rail-section.vue'
import Divider from '@/components/divider.vue'
import FactList from '@/components/fact-list.vue'
import NoteList from '@/components/note-list.vue'
import Table from '@/components/table.vue'
import TableBody from '@/components/table-body.vue'
import TableCell from '@/components/table-cell.vue'
import TableHead from '@/components/table-head.vue'
import TableHeader from '@/components/table-header.vue'
import TableRow from '@/components/table-row.vue'
import TextLink from '@/components/text-link.vue'
import Timeline from '@/components/timeline.vue'
import TimelineItem from '@/components/timeline-item.vue'

const lines = [
  { item: 'Scale plan, 12 seats', quantity: 12, price: '$200.00', total: '$2,400.00' },
  { item: 'Priority support, yearly', quantity: 1, price: '$600.00', total: '$600.00' },
  { item: 'Onboarding session', quantity: 2, price: '$250.00', total: '$500.00' },
]

const totals = [
  { label: 'Subtotal', value: '$3,500.00' },
  { label: 'VAT (20%)', value: '$700.00' },
  { label: 'Paid', value: '$0.00' },
]

const notes = [
  {
    id: 'n1',
    body: 'Finance at Lumen Health moves to a new card on October 10. Hold the next retry until then.',
    author: 'Awa Diallo',
    date: 'Oct 3, 2026',
    pinned: true,
  },
  { id: 'n2', body: 'PO number requested for their records, sent by email.', author: 'Kofi Mensah', date: 'Sep 19, 2026' },
]

const sectionTitle = 'text-lg/7 font-semibold text-zinc-950 sm:text-base/6 dark:text-white'
</script>

<template>
  <Breadcrumbs>
    <BreadcrumbItem href="/invoices">Invoices</BreadcrumbItem>
    <BreadcrumbItem>INV-2041</BreadcrumbItem>
  </Breadcrumbs>
  <DetailHeader class="mt-4" size="lg" title="$4,200.00" id="INV-2041" id-label="Copy invoice number">
    <template #badges><Badge color="red">Overdue</Badge></template>
    <template #meta>Due Oct 2, 2026</template>
    <template #actions>
      <Button outline class="max-sm:flex-1">
        <BellAlertIcon />
        Send reminder
      </Button>
      <Button class="max-sm:flex-1">
        <CheckIcon />
        Record payment
      </Button>
    </template>
  </DetailHeader>

  <DetailLayout class="mt-10" aside-label="Invoice summary">
    <section aria-labelledby="line-items">
      <h2 id="line-items" :class="sectionTitle">Line items</h2>
      <Table dense class="mt-4">
        <TableHead>
          <TableRow>
            <TableHeader>Item</TableHeader>
            <TableHeader class="text-right">Qty</TableHeader>
            <TableHeader class="text-right">Unit price</TableHeader>
            <TableHeader class="text-right">Amount</TableHeader>
          </TableRow>
        </TableHead>
        <TableBody>
          <TableRow v-for="line in lines" :key="line.item">
            <TableCell class="font-medium">{{ line.item }}</TableCell>
            <TableCell class="text-right tabular-nums">{{ line.quantity }}</TableCell>
            <TableCell class="text-right tabular-nums">{{ line.price }}</TableCell>
            <TableCell class="text-right tabular-nums">{{ line.total }}</TableCell>
          </TableRow>
        </TableBody>
      </Table>
      <dl class="mt-6 ml-auto max-w-xs text-base/6 sm:text-sm/6">
        <div v-for="row in totals" :key="row.label" class="flex justify-between gap-4 py-1.5">
          <dt class="text-zinc-500 dark:text-zinc-400">{{ row.label }}</dt>
          <dd class="text-zinc-950 tabular-nums dark:text-white">{{ row.value }}</dd>
        </div>
        <div class="mt-2 flex justify-between gap-4 border-t border-zinc-950/10 pt-3 dark:border-white/10">
          <dt class="font-semibold text-zinc-950 dark:text-white">Amount due</dt>
          <dd class="font-semibold text-zinc-950 tabular-nums dark:text-white">$4,200.00</dd>
        </div>
      </dl>
    </section>

    <Divider soft class="my-10" />

    <section aria-labelledby="activity">
      <h2 id="activity" :class="sectionTitle">Activity</h2>
      <Timeline class="mt-6">
        <TimelineItem :icon="BellAlertIcon" color="amber" title="Reminder sent to arthur@lumenhealth.io" time="Oct 5" date-time="2026-10-05" />
        <TimelineItem
          :icon="XCircleIcon"
          color="red"
          title="Automatic payment failed"
          description="Mastercard •••• 5100 was declined for insufficient funds."
          time="Oct 2"
          date-time="2026-10-02"
        />
        <TimelineItem :icon="EyeIcon" title="Viewed by the customer" time="Sep 20" date-time="2026-09-20" />
        <TimelineItem :icon="PaperAirplaneIcon" color="sky" title="Sent to arthur@lumenhealth.io" time="Sep 18" date-time="2026-09-18" />
        <TimelineItem :icon="DocumentTextIcon" title="Created by Awa Diallo" time="Sep 18" date-time="2026-09-18" />
      </Timeline>
    </section>

    <Divider soft class="my-10" />

    <section aria-labelledby="notes">
      <h2 id="notes" :class="sectionTitle">Notes</h2>
      <NoteList class="mt-4" :notes="notes" />
    </section>

    <template #aside>
      <DetailRail>
        <DetailRailSection label="Billed to">
          <div class="flex min-w-0 items-center gap-3">
            <Avatar src="/avatars/arthur.jpg" class="size-10 shrink-0" />
            <div class="min-w-0 text-base/6 sm:text-sm/6">
              <p class="truncate font-medium text-zinc-950 dark:text-white">Arthur Monney</p>
              <p class="truncate text-zinc-500 dark:text-zinc-400">Lumen Health</p>
            </div>
          </div>
          <p class="mt-3 text-base/6 sm:text-sm/6">
            <TextLink href="/customers/cus_8Hq2Lm">View customer</TextLink>
          </p>
        </DetailRailSection>
        <DetailRailSection label="Payment">
          <FactList
            layout="rows"
            :facts="[
              { key: 'method', label: 'Method', value: 'Mastercard •••• 5100' },
              { key: 'terms', label: 'Terms', value: 'Net 14' },
              { key: 'issued', label: 'Issued', value: 'Sep 18, 2026' },
              { key: 'due', label: 'Due', value: 'Oct 2, 2026' },
            ]"
          />
        </DetailRailSection>
        <DetailRailSection label="Details">
          <FactList
            layout="rows"
            :facts="[
              { key: 'currency', label: 'Currency', value: 'USD' },
              { key: 'po', label: 'PO number' },
              { key: 'language', label: 'Language', value: 'English' },
            ]"
          />
        </DetailRailSection>
      </DetailRail>
    </template>
  </DetailLayout>
</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 { useId } from 'react'

export function DetailLayout({
  aside,
  asideLabel,
  asideWidth = '20rem',
  sticky = true,
  className,
  style,
  children,
  ...props
}: {
  aside: React.ReactNode
  asideLabel: string
  asideWidth?: string
  sticky?: boolean
} & React.ComponentPropsWithoutRef<'div'>) {
  return (
    <div
      {...props}
      style={{ '--aside-width': asideWidth, ...style } as React.CSSProperties}
      className={clsx(className, 'grid gap-10 lg:grid-cols-[minmax(0,1fr)_var(--aside-width)] lg:gap-12')}
    >
      <div className="min-w-0">{children}</div>
      <aside
        aria-label={asideLabel}
        className={clsx('min-w-0', sticky && 'lg:sticky lg:top-[var(--sticky-top,6rem)] lg:self-start')}
      >
        {aside}
      </aside>
    </div>
  )
}

export function DetailRail({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
  return <div {...props} className={clsx(className, 'divide-y divide-zinc-950/5 dark:divide-white/5')} />
}

export function DetailRailSection({
  label,
  className,
  children,
  ...props
}: { label: string } & React.ComponentPropsWithoutRef<'section'>) {
  let labelId = useId()

  return (
    <section aria-labelledby={labelId} {...props} className={clsx(className, 'py-5 first:pt-0 last:pb-0')}>
      <h2 id={labelId} className="text-sm/6 font-medium tracking-wide text-zinc-500 uppercase sm:text-xs/6 dark:text-zinc-400">
        {label}
      </h2>
      <div className="mt-3">{children}</div>
    </section>
  )
}
<script setup lang="ts">
withDefaults(
  defineProps<{
    asideLabel: string
    asideWidth?: string
    sticky?: boolean
  }>(),
  { asideWidth: '20rem', sticky: true }
)
</script>

<template>
  <div
    class="grid gap-10 lg:grid-cols-[minmax(0,1fr)_var(--aside-width)] lg:gap-12"
    :style="{ '--aside-width': asideWidth }"
  >
    <div class="min-w-0"><slot /></div>
    <aside
      :aria-label="asideLabel"
      class="min-w-0"
      :class="sticky && 'lg:sticky lg:top-[var(--sticky-top,6rem)] lg:self-start'"
    >
      <slot name="aside" />
    </aside>
  </div>
</template>

Component API

PropDefaultDescription
DetailLayoutrenders a <div> element
children-The record: its sections, divided by soft rules.
aside-The summary column.
asideLabel-Accessible name of that column: `Invoice summary`.
asideWidth'20rem'Width of the column from `lg`.
stickytrueKeep the column in view while the record scrolls. It sits under `--sticky-top` when the page sets it, 6rem otherwise.
DetailRailrenders a <div> element
This component does not expose any component-specific props.
DetailRailSectionrenders a <section> element
label-Small caps heading of the section, and its accessible name.
PropDefaultDescription
DetailLayoutrenders a <div> element
#default-The record: its sections, divided by soft rules.
#aside-The summary column.
asideLabel-Accessible name of that column: `Invoice summary`.
asideWidth'20rem'Width of the column from `lg`.
stickytrueKeep the column in view while the record scrolls. It sits under `--sticky-top` when the page sets it, 6rem otherwise.
DetailRailrenders a <div> element
This component does not expose any component-specific props.
DetailRailSectionrenders a <section> element
label-Small caps heading of the section, and its accessible name.

Composition

The main column holds what people work on: the lines of an invoice, what happened to it, the notes on it. Each block is a section with its own h2, separated by a soft Divider. The rail holds what they glance at while they work: who the record belongs to, how it is paid, the settings that rarely change. Each topic is a DetailRailSection, headed in small caps so the eye jumps from label to label.

Keep the rail short enough to fit in the viewport, it stays in view while the main column scrolls. Under lg it moves below the record, in the same order.

A customer record

The same layout carries a different kind of record. Here the main column opens on a StatBand and lists the invoices, while the rail keeps the contacts, the billing address and the tax details at hand.

import { DocumentPlusIcon, PencilIcon } from '@heroicons/react/16/solid'
import { Avatar } from '@/components/avatar'
import { Badge } from '@/components/badge'
import { BreadcrumbItem, Breadcrumbs } from '@/components/breadcrumbs'
import { Button } from '@/components/button'
import { DetailHeader } from '@/components/detail-header'
import { DetailLayout, DetailRail, DetailRailSection } from '@/components/detail-layout'
import { Divider } from '@/components/divider'
import { FactList } from '@/components/fact-list'
import { StatBand, StatTile } from '@/components/stat-band'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/table'

const invoices = [
  { id: 'INV-2041', issued: 'Sep 18, 2026', amount: '$4,200.00', status: 'Overdue' },
  { id: 'INV-1987', issued: 'Aug 18, 2026', amount: '$4,200.00', status: 'Paid' },
  { id: 'INV-1932', issued: 'Jul 18, 2026', amount: '$3,960.00', status: 'Paid' },
  { id: 'INV-1880', issued: 'Jun 18, 2026', amount: '$3,960.00', status: 'Paid' },
] as const

const statusColor = { Paid: 'green', Overdue: 'red' } as const

const contacts = [
  { name: 'Arthur Monney', role: 'Billing contact', avatar: '/avatars/arthur.jpg' },
  { name: 'Leonel Njoya', role: 'Finance', avatar: '/avatars/leonel.jpg' },
]

export default function Customer() {
  return (
    <>
      <Breadcrumbs>
        <BreadcrumbItem href="/customers">Customers</BreadcrumbItem>
        <BreadcrumbItem>Lumen Health</BreadcrumbItem>
      </Breadcrumbs>
      <DetailHeader
        className="mt-4"
        avatar={<Avatar initials="LH" square className="size-12 shrink-0 bg-teal-600 text-white" />}
        title="Lumen Health"
        badges={<Badge color="red">1 overdue</Badge>}
        id="cus_8Hq2Lm"
        idLabel="Copy customer ID"
        meta="Customer since March 2024"
        actions={
          <>
            <Button outline className="max-sm:flex-1">
              <PencilIcon />
              Edit
            </Button>
            <Button className="max-sm:flex-1">
              <DocumentPlusIcon />
              New invoice
            </Button>
          </>
        }
      />

      <DetailLayout
        className="mt-10"
        asideLabel="Customer summary"
        aside={
          <DetailRail>
            <DetailRailSection label="Contacts">
              <ul role="list" className="space-y-4">
                {contacts.map((contact) => (
                  <li key={contact.name} className="flex min-w-0 items-center gap-3">
                    <Avatar src={contact.avatar} className="size-9 shrink-0" />
                    <div className="min-w-0 text-base/6 sm:text-sm/6">
                      <p className="truncate font-medium text-zinc-950 dark:text-white">{contact.name}</p>
                      <p className="truncate text-zinc-500 dark:text-zinc-400">{contact.role}</p>
                    </div>
                  </li>
                ))}
              </ul>
            </DetailRailSection>
            <DetailRailSection label="Billing address">
              <address className="text-base/6 text-zinc-950 not-italic sm:text-sm/6 dark:text-white">
                12 rue des Tanneurs
                <br />
                69002 Lyon
                <br />
                France
              </address>
            </DetailRailSection>
            <DetailRailSection label="Details">
              <FactList
                layout="rows"
                facts={[
                  { label: 'Tax ID', value: 'FR 40 303 265 045', mono: true, copy: true },
                  { label: 'Currency', value: 'USD' },
                  { label: 'Terms', value: 'Net 14' },
                ]}
              />
            </DetailRailSection>
          </DetailRail>
        }
      >
        <StatBand columns={3}>
          <StatTile label="Lifetime value" value="$48,720" context="Since March 2024" />
          <StatTile label="Open balance" value="$4,200" context="1 invoice past due" />
          <StatTile label="Average time to pay" value="9 days" context="Terms are Net 14" />
        </StatBand>

        <Divider soft className="my-10" />

        <section aria-labelledby="invoices">
          <h2 id="invoices" className="text-lg/7 font-semibold text-zinc-950 sm:text-base/6 dark:text-white">
            Invoices
          </h2>
          <Table dense className="mt-4">
            <TableHead>
              <TableRow>
                <TableHeader>Number</TableHeader>
                <TableHeader>Issued</TableHeader>
                <TableHeader>Status</TableHeader>
                <TableHeader className="text-right">Amount</TableHeader>
              </TableRow>
            </TableHead>
            <TableBody>
              {invoices.map((invoice) => (
                <TableRow key={invoice.id} href={`/invoices/${invoice.id}`} title={`Open ${invoice.id}`}>
                  <TableCell className="font-mono">{invoice.id}</TableCell>
                  <TableCell className="text-zinc-500 dark:text-zinc-400">{invoice.issued}</TableCell>
                  <TableCell>
                    <Badge color={statusColor[invoice.status]}>{invoice.status}</Badge>
                  </TableCell>
                  <TableCell className="text-right tabular-nums">{invoice.amount}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </section>
      </DetailLayout>
    </>
  )
}
<script setup lang="ts">
import { DocumentPlusIcon, PencilIcon } from '@heroicons/vue/16/solid'
import Avatar from '@/components/avatar.vue'
import Badge from '@/components/badge.vue'
import BreadcrumbItem from '@/components/breadcrumb-item.vue'
import Breadcrumbs from '@/components/breadcrumbs.vue'
import Button from '@/components/button.vue'
import DetailHeader from '@/components/detail-header.vue'
import DetailLayout from '@/components/detail-layout.vue'
import DetailRail from '@/components/detail-rail.vue'
import DetailRailSection from '@/components/detail-rail-section.vue'
import Divider from '@/components/divider.vue'
import FactList from '@/components/fact-list.vue'
import StatBand from '@/components/stat-band.vue'
import StatTile from '@/components/stat-tile.vue'
import Table from '@/components/table.vue'
import TableBody from '@/components/table-body.vue'
import TableCell from '@/components/table-cell.vue'
import TableHead from '@/components/table-head.vue'
import TableHeader from '@/components/table-header.vue'
import TableRow from '@/components/table-row.vue'

const invoices = [
  { id: 'INV-2041', issued: 'Sep 18, 2026', amount: '$4,200.00', status: 'Overdue' },
  { id: 'INV-1987', issued: 'Aug 18, 2026', amount: '$4,200.00', status: 'Paid' },
  { id: 'INV-1932', issued: 'Jul 18, 2026', amount: '$3,960.00', status: 'Paid' },
  { id: 'INV-1880', issued: 'Jun 18, 2026', amount: '$3,960.00', status: 'Paid' },
] as const

const statusColor = { Paid: 'green', Overdue: 'red' } as const

const contacts = [
  { name: 'Arthur Monney', role: 'Billing contact', avatar: '/avatars/arthur.jpg' },
  { name: 'Leonel Njoya', role: 'Finance', avatar: '/avatars/leonel.jpg' },
]
</script>

<template>
  <Breadcrumbs>
    <BreadcrumbItem href="/customers">Customers</BreadcrumbItem>
    <BreadcrumbItem>Lumen Health</BreadcrumbItem>
  </Breadcrumbs>
  <DetailHeader class="mt-4" title="Lumen Health" id="cus_8Hq2Lm" id-label="Copy customer ID">
    <template #avatar><Avatar initials="LH" square class="size-12 shrink-0 bg-teal-600 text-white" /></template>
    <template #badges><Badge color="red">1 overdue</Badge></template>
    <template #meta>Customer since March 2024</template>
    <template #actions>
      <Button outline class="max-sm:flex-1">
        <PencilIcon />
        Edit
      </Button>
      <Button class="max-sm:flex-1">
        <DocumentPlusIcon />
        New invoice
      </Button>
    </template>
  </DetailHeader>

  <DetailLayout class="mt-10" aside-label="Customer summary">
    <StatBand :columns="3">
      <StatTile label="Lifetime value" value="$48,720" context="Since March 2024" />
      <StatTile label="Open balance" value="$4,200" context="1 invoice past due" />
      <StatTile label="Average time to pay" value="9 days" context="Terms are Net 14" />
    </StatBand>

    <Divider soft class="my-10" />

    <section aria-labelledby="invoices">
      <h2 id="invoices" class="text-lg/7 font-semibold text-zinc-950 sm:text-base/6 dark:text-white">Invoices</h2>
      <Table dense class="mt-4">
        <TableHead>
          <TableRow>
            <TableHeader>Number</TableHeader>
            <TableHeader>Issued</TableHeader>
            <TableHeader>Status</TableHeader>
            <TableHeader class="text-right">Amount</TableHeader>
          </TableRow>
        </TableHead>
        <TableBody>
          <TableRow v-for="invoice in invoices" :key="invoice.id" :href="`/invoices/${invoice.id}`" :title="`Open ${invoice.id}`">
            <TableCell class="font-mono">{{ invoice.id }}</TableCell>
            <TableCell class="text-zinc-500 dark:text-zinc-400">{{ invoice.issued }}</TableCell>
            <TableCell>
              <Badge :color="statusColor[invoice.status]">{{ invoice.status }}</Badge>
            </TableCell>
            <TableCell class="text-right tabular-nums">{{ invoice.amount }}</TableCell>
          </TableRow>
        </TableBody>
      </Table>
    </section>

    <template #aside>
      <DetailRail>
        <DetailRailSection label="Contacts">
          <ul role="list" class="space-y-4">
            <li v-for="contact in contacts" :key="contact.name" class="flex min-w-0 items-center gap-3">
              <Avatar :src="contact.avatar" class="size-9 shrink-0" />
              <div class="min-w-0 text-base/6 sm:text-sm/6">
                <p class="truncate font-medium text-zinc-950 dark:text-white">{{ contact.name }}</p>
                <p class="truncate text-zinc-500 dark:text-zinc-400">{{ contact.role }}</p>
              </div>
            </li>
          </ul>
        </DetailRailSection>
        <DetailRailSection label="Billing address">
          <address class="text-base/6 text-zinc-950 not-italic sm:text-sm/6 dark:text-white">
            12 rue des Tanneurs<br />
            69002 Lyon<br />
            France
          </address>
        </DetailRailSection>
        <DetailRailSection label="Details">
          <FactList
            layout="rows"
            :facts="[
              { key: 'tax', label: 'Tax ID', value: 'FR 40 303 265 045', mono: true, copy: true },
              { key: 'currency', label: 'Currency', value: 'USD' },
              { key: 'terms', label: 'Terms', value: 'Net 14' },
            ]"
          />
        </DetailRailSection>
      </DetailRail>
    </template>
  </DetailLayout>
</template>

Customization

Sticky offset

The rail sticks under the app header. Set --sticky-top on a wrapper to the height of that header, the layout reads it. Without it the rail sticks 6rem from the top.

<div className="[--sticky-top:--spacing(24)]">
  <DetailLayout asideLabel="Invoice summary" aside={…}>…</DetailLayout>
</div>
<div class="[--sticky-top:--spacing(24)]">
  <DetailLayout aside-label="Invoice summary">…</DetailLayout>
</div>

Another aside

The aside takes anything, a card or a form summary. Narrow it with asideWidth, widths between 17rem and 22rem read well, and turn sticky off when the aside is taller than the viewport.

<DetailLayout asideLabel="Plan" asideWidth="18rem" sticky={false} aside={<SoftCard title="Scale plan">…</SoftCard>}>
  …
</DetailLayout>
<DetailLayout aside-label="Plan" aside-width="18rem" :sticky="false">
  …
  <template #aside>
    <SoftCard title="Scale plan">…</SoftCard>
  </template>
</DetailLayout>