← Dashboard
Zubeyr··Pagesdetaillayout

Order Detail

A record detail page: header with status badges and navigation, a customer note, line items, a payment summary and a side panel with contact, address, conversion, tags and timeline.

Live demo ↗

Installation

Run the shadcn CLI in a project that already has components.json. Using the @tomei tab? Register the namespace once first.

pnpm dlx shadcn@latest add https://tomei.zubeyr.dev/ui/r/order-detail.json
  • →Installs lucide-react from npm.
  • →Pulls in sidebar.
  • →Pulls in button.
  • →Pulls in badge.
  • →Pulls in timeline.
  • →Files land in components/dashboard/order-detail.

Installing by hand? Add the dependencies, then copy the source code below.

terminal.sh
pnpm add lucide-react

Usage

example.tsx
import { OrderDetail } from '@/components/dashboard/order-detail/order-detail'

export default function Demo() {
  return <OrderDetail />
}

Source code

order-detail.tsx
import {
  BarChart3,
  ChevronDown,
  ChevronUp,
  Globe,
  Home,
  Mail,
  MapPin,
  MoreHorizontal,
  Package,
  Pencil,
  Plus,
  Printer,
  ShoppingBag,
  Truck,
  Users,
} from 'lucide-react'

import { Badge } from '@/registry/dashboard/components/badge/badge'
import { Button } from '@/registry/dashboard/components/button/button'
import { InfoRow, InfoSection } from './info-section'
import { Note } from './note'
import { PageHeader } from './page-header'
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarHeader,
  SidebarItem,
} from '@/registry/dashboard/components/sidebar/sidebar'
import { SummaryList } from './summary-list'
import { LineItems } from './line-items'
import { Timeline } from '@/registry/dashboard/blocks/timeline/timeline'

import { customer, events, items, tags } from './data'

export function OrderDetail() {
  return (
    <div className="flex h-svh overflow-hidden bg-sidebar text-foreground">
      <Sidebar className="border-r-0 max-lg:hidden">
        <SidebarHeader
          logo={<span className="size-8 rounded-lg bg-sidebar-primary-foreground" />}
          title="Tomei"
          subtitle="Store admin"
        />
        <SidebarContent>
          <SidebarGroup>
            <SidebarItem icon={<Home />}>Home</SidebarItem>
            <SidebarItem icon={<ShoppingBag />} count={12} active>
              Orders
            </SidebarItem>
            <SidebarItem icon={<Package />}>Products</SidebarItem>
            <SidebarItem icon={<Users />}>Customers</SidebarItem>
            <SidebarItem icon={<BarChart3 />}>Analytics</SidebarItem>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <SidebarItem icon={<Globe />}>Online store</SidebarItem>
        </SidebarFooter>
      </Sidebar>

      <main className="min-w-0 flex-1 overflow-y-auto bg-card lg:my-2 lg:mr-2 lg:rounded-2xl lg:border">
        <div className="mx-auto flex max-w-[1120px] flex-col gap-8 p-6">
          <PageHeader
            backHref="#"
            breadcrumb={
              <span className="flex items-center gap-1.5">
                <ShoppingBag className="size-4" /> Orders
              </span>
            }
            tools={
              <>
                <Button size="icon" aria-label="Print">
                  <Printer />
                </Button>
                <Button size="icon" aria-label="Edit">
                  <Pencil />
                </Button>
                <Button size="icon" aria-label="More actions">
                  <MoreHorizontal />
                </Button>
                <Button size="icon" aria-label="Previous order">
                  <ChevronUp />
                </Button>
                <Button size="icon" aria-label="Next order">
                  <ChevronDown />
                </Button>
              </>
            }
            title="Order #1078"
            badges={
              <>
                <Badge indicator="dot">Paid</Badge>
                <Badge tone="warning" indicator="ring">
                  Unfulfilled
                </Badge>
              </>
            }
            subtitle="Oct 29, 2026 at 7:24 PM via Online Store"
          />

          <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_20rem]">
            <div className="flex min-w-0 flex-col gap-8">
              <Note label="Note from customer">
                Please delay shipping by two days, I&apos;m away and can&apos;t collect the package.
                Thanks!
              </Note>

              <LineItems
                items={items}
                status={
                  <Badge tone="warning" indicator="ring">
                    Unfulfilled
                  </Badge>
                }
                actions={
                  <>
                    <Button variant="ghost" size="icon" aria-label="More fulfillment actions">
                      <MoreHorizontal />
                    </Button>
                    <Button>Mark as fulfilled</Button>
                    <Button variant="primary">Create shipping label</Button>
                  </>
                }
                meta={
                  <>
                    <span className="flex items-center gap-2">
                      <Truck /> Express
                    </span>
                    <span className="flex items-center gap-2">
                      <MapPin /> Main warehouse
                    </span>
                  </>
                }
              />

              <section className="flex flex-col gap-3">
                <h2 className="text-base font-semibold">Payment</h2>
                <SummaryList
                  rows={[
                    { label: 'Subtotal', detail: '4 items', value: '$397.50' },
                    { label: 'Discount', detail: 'WELCOME10', value: '−$30.00' },
                    { label: 'Shipping', detail: 'Express', value: '$4.99' },
                  ]}
                  total={{ label: 'Total', value: '$372.49' }}
                  footer={[
                    { label: 'Store credit', value: '$50.00' },
                    { label: 'Visa •••• 3840', value: '$322.49' },
                  ]}
                />
              </section>
            </div>

            <aside className="flex flex-col">
              <InfoSection>
                <h2 className="text-lg font-semibold">{customer.name}</h2>
                <InfoRow icon={<Mail />}>{customer.email}</InfoRow>
                <InfoRow icon={<ShoppingBag />}>{customer.orders} orders</InfoRow>
              </InfoSection>

              <InfoSection title="Shipping address">
                <address className="text-sm leading-6 not-italic">
                  {customer.address.map((line) => (
                    <span key={line} className="block">
                      {line}
                    </span>
                  ))}
                </address>
              </InfoSection>

              <InfoSection title="Conversion summary">
                <InfoRow icon={<ShoppingBag />}>{customer.orders}th order</InfoRow>
                <InfoRow icon={<Globe />}>Source: direct</InfoRow>
              </InfoSection>

              <InfoSection
                title="Tags"
                action={
                  <Button variant="ghost" size="icon-sm" aria-label="Add tag">
                    <Plus />
                  </Button>
                }
              >
                <div className="flex flex-wrap gap-1.5">
                  {tags.map((tag) => (
                    <Badge key={tag}>{tag}</Badge>
                  ))}
                </div>
              </InfoSection>

              <InfoSection title="Timeline">
                <Timeline events={events} />
              </InfoSection>
            </aside>
          </div>
        </div>
      </main>
    </div>
  )
}
data.tsx
import type { TimelineEvent } from '@/registry/dashboard/blocks/timeline/timeline'

import type { LineItem } from './line-items'

const swatch = (color: string) => <span className="size-full" style={{ background: color }} />

export const items: LineItem[] = [
  {
    id: '1',
    name: 'Canvas Sneakers',
    variant: '10.5 US',
    price: 235,
    quantity: 1,
    image: swatch('#d6cfc4'),
  },
  {
    id: '2',
    name: 'Button-down Shirt',
    variant: 'Blue / XL',
    price: 65,
    quantity: 2,
    image: swatch('#3b4a6b'),
  },
  {
    id: '3',
    name: 'Knit Beanie',
    variant: 'One size',
    price: 32.5,
    quantity: 1,
    image: swatch('#6b7a4f'),
  },
]

export const customer = {
  name: 'Jordan Ellis',
  email: 'jordan@example.com',
  orders: 4,
  address: ['Jordan Ellis', '184 Kent Ave', 'Brooklyn, NY', 'United States'],
}

export const tags = ['VIP', 'Wholesale']

export const events: TimelineEvent[] = [
  { id: '1', content: 'Order confirmation email was sent to Jordan Ellis.', time: '1:36 PM' },
  { id: '2', content: 'A $322.49 payment was processed on Visa •••• 3840.', time: '1:36 PM' },
  { id: '3', content: 'Jordan Ellis placed this order on the Online Store.', time: '1:35 PM' },
]
info-section.tsx
import { cn } from '@/lib/utils'

type InfoSectionProps = {
  title?: React.ReactNode
  action?: React.ReactNode
  className?: string
  children?: React.ReactNode
}

export function InfoSection({ title, action, className, children }: InfoSectionProps) {
  return (
    <section
      className={cn('flex flex-col gap-3 border-b py-5 first:pt-0 last:border-0', className)}
    >
      {(title || action) && (
        <div className="flex items-center justify-between gap-2">
          {title && <h2 className="text-sm text-muted-foreground">{title}</h2>}
          {action}
        </div>
      )}
      {children}
    </section>
  )
}

export function InfoRow({
  icon,
  className,
  children,
}: {
  icon?: React.ReactNode
  className?: string
  children: React.ReactNode
}) {
  return (
    <p
      className={cn(
        'flex min-w-0 items-center gap-2.5 text-sm [&_svg]:size-4 [&_svg]:shrink-0',
        className,
      )}
    >
      {icon && <span className="flex text-muted-foreground">{icon}</span>}
      <span className="min-w-0 truncate">{children}</span>
    </p>
  )
}
line-items.tsx
import { cn } from '@/lib/utils'

export type LineItem = {
  id: string
  name: React.ReactNode
  image?: React.ReactNode
  variant?: React.ReactNode
  price: number
  quantity: number
}

type LineItemsProps = {
  items: LineItem[]
  status?: React.ReactNode
  actions?: React.ReactNode
  meta?: React.ReactNode
  formatOptions?: Intl.NumberFormatOptions
  className?: string
}

export function LineItems({
  items,
  status,
  actions,
  meta,
  formatOptions = { style: 'currency', currency: 'USD' },
  className,
}: LineItemsProps) {
  const money = new Intl.NumberFormat('en-US', formatOptions)

  return (
    <section className={cn('flex flex-col gap-3', className)}>
      {(status || actions) && (
        <div className="flex flex-wrap items-center justify-between gap-2">
          <div className="flex items-center gap-2 text-base font-semibold">{status}</div>
          {actions && <div className="flex items-center gap-2">{actions}</div>}
        </div>
      )}
      <div className="rounded-xl bg-card shadow-(--shadow-card)">
        {meta && (
          <div className="flex items-center justify-between gap-3 border-b px-4 py-3 text-sm text-muted-foreground [&_svg]:size-4">
            {meta}
          </div>
        )}
        <ul className="divide-y px-4">
          {items.map((item) => (
            <li
              key={item.id}
              className="grid grid-cols-[auto_1fr_auto_auto] items-center gap-4 py-3"
            >
              <span className="flex size-12 items-center justify-center overflow-hidden rounded-lg bg-muted">
                {item.image}
              </span>
              <span className="flex min-w-0 flex-col items-start gap-1.5">
                <span className="truncate text-sm font-medium">{item.name}</span>
                {item.variant && (
                  <span className="rounded-md bg-muted px-1.5 py-0.5 text-xs">{item.variant}</span>
                )}
              </span>
              <span className="flex items-center gap-1.5 text-sm text-muted-foreground tabular-nums max-sm:hidden">
                {money.format(item.price)} ×
                <span className="rounded bg-muted px-1.5 text-xs text-foreground">
                  {item.quantity}
                </span>
              </span>
              <span className="w-20 text-right text-sm tabular-nums">
                {money.format(item.price * item.quantity)}
              </span>
            </li>
          ))}
        </ul>
      </div>
    </section>
  )
}
note.tsx
import { cn } from '@/lib/utils'

const tones = {
  yellow: 'bg-yellow-200/80 dark:bg-yellow-400/25',
  blue: 'bg-sky-200/80 dark:bg-sky-400/25',
  pink: 'bg-pink-200/80 dark:bg-pink-400/25',
}

type NoteProps = {
  label?: React.ReactNode
  tone?: keyof typeof tones
  className?: string
  children: React.ReactNode
}

export function Note({ label, tone = 'yellow', className, children }: NoteProps) {
  return (
    <figure className={cn('flex flex-col gap-1.5 border-l pl-3', className)}>
      {label && <figcaption className="text-xs text-muted-foreground">{label}</figcaption>}
      <blockquote className="text-sm leading-7">
        <span className={cn('box-decoration-clone px-1.5 py-1', tones[tone])}>{children}</span>
      </blockquote>
    </figure>
  )
}
page-header.tsx
import { cn } from '@/lib/utils'

type PageHeaderProps = {
  title: React.ReactNode
  badges?: React.ReactNode
  subtitle?: React.ReactNode
  backHref?: string
  breadcrumb?: React.ReactNode
  tools?: React.ReactNode
  actions?: React.ReactNode
  className?: string
}

export function PageHeader({
  title,
  badges,
  subtitle,
  backHref,
  breadcrumb,
  tools,
  actions,
  className,
}: PageHeaderProps) {
  return (
    <header className={cn('flex flex-col gap-6', className)}>
      {(backHref || breadcrumb || tools) && (
        <div className="flex items-center justify-between gap-3">
          <div className="flex items-center gap-2 text-sm text-muted-foreground">
            {backHref && (
              <a
                href={backHref}
                aria-label="Back"
                className="flex size-8 items-center justify-center rounded-full bg-card text-foreground shadow-(--shadow-button) hover:bg-muted"
              >
                <svg viewBox="0 0 16 16" fill="none" aria-hidden className="size-4">
                  <path
                    d="M10 3 5 8l5 5"
                    stroke="currentColor"
                    strokeWidth="1.6"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              </a>
            )}
            {breadcrumb}
          </div>
          {tools && <div className="flex items-center gap-1.5">{tools}</div>}
        </div>
      )}
      <div className="flex flex-wrap items-end justify-between gap-4">
        <div className="flex min-w-0 flex-col gap-2">
          <div className="flex flex-wrap items-center gap-2">
            <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
            {badges}
          </div>
          {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
        </div>
        {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
      </div>
    </header>
  )
}
summary-list.tsx
import { cn } from '@/lib/utils'

export type SummaryRow = {
  label: React.ReactNode
  detail?: React.ReactNode
  value: React.ReactNode
}

type SummaryListProps = {
  rows: SummaryRow[]
  total?: { label: React.ReactNode; value: React.ReactNode }
  footer?: SummaryRow[]
  className?: string
}

function Row({ label, detail, value, strong }: SummaryRow & { strong?: boolean }) {
  return (
    <div
      className={cn(
        'grid grid-cols-[1fr_auto_6rem] items-center gap-4 py-2 text-sm',
        strong && 'font-semibold',
      )}
    >
      <span>{label}</span>
      <span className="text-right text-muted-foreground">{detail}</span>
      <span className="text-right tabular-nums">{value}</span>
    </div>
  )
}

export function SummaryList({ rows, total, footer, className }: SummaryListProps) {
  return (
    <div className={cn('rounded-xl bg-muted', className)}>
      <div className="rounded-xl bg-card px-4 py-2 shadow-(--shadow-card)">
        {rows.map((row, i) => (
          <Row key={i} {...row} />
        ))}
        {total && (
          <div className="mt-1 border-t pt-1">
            <Row label={total.label} value={total.value} strong />
          </div>
        )}
      </div>
      {footer?.length ? (
        <div className="px-4 py-2">
          {footer.map((row, i) => (
            <Row key={i} {...row} />
          ))}
        </div>
      ) : null}
    </div>
  )
}