# Description List

> Semantic key/value detail list in vertical, horizontal and grid layouts with copy buttons, badges, links and empty placeholders.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/description-list
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/description-list.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/description-list-demo.tsx"
import { DescriptionList, type DescriptionItem } from "@/components/description-list"

const order: DescriptionItem[] = [
  { label: "Order ID", value: "ORD-2026-004817", copyable: true, mono: true },
  { label: "Status", value: "Paid", badge: "success" },
  { label: "Customer", value: "Ada Lovelace", href: "mailto:ada@analytical.engine" },
  { label: "Total", value: "$128.50" },
  { label: "Placed on", value: "Nov 3, 2026, 9:41 AM" },
  { label: "Tracking number", value: null },
]

const profile: DescriptionItem[] = [
  { label: "Full name", value: "Grace Hopper" },
  { label: "Email", value: "grace@navy.example", copyable: true },
  { label: "Role", value: "Admin", badge: "violet" },
  { label: "Phone", value: "" },
  { label: "Website", value: "grace.example.com", href: "https://grace.example.com" },
  { label: "Location", value: "Arlington, VA" },
  { label: "Bio", value: "Pioneer of machine-independent programming languages and long-time advocate for readable code.", wide: true },
]

const subscription: DescriptionItem[] = [
  { label: "Plan", value: "Pro, billed yearly", badge: "info" },
  { label: "Renews", value: "Dec 7, 2026" },
  { label: "API key", value: "sk_live_••••••••4f2a", copyText: "sk_live_demo_51Hx0aQf4f2a", copyable: true, mono: true },
]

export default function DescriptionListDemo() {
  return (
    <div className="grid w-full max-w-4xl min-w-0 grid-cols-[minmax(0,1fr)] gap-5 md:grid-cols-2">
      <section className="min-w-0 rounded-2xl border bg-card p-5 shadow-sm">
        <h3 className="mb-4 text-sm font-semibold">Order details</h3>
        <DescriptionList items={order} layout="horizontal" />
      </section>
      <section className="min-w-0 rounded-2xl border bg-card p-5 shadow-sm">
        <h3 className="mb-4 text-sm font-semibold">Subscription</h3>
        <DescriptionList items={subscription} layout="vertical" />
      </section>
      <section className="min-w-0 rounded-2xl border bg-card p-5 shadow-sm md:col-span-2">
        <h3 className="mb-4 text-sm font-semibold">User profile</h3>
        <DescriptionList items={profile} layout="grid" />
      </section>
    </div>
  )
}
```

## Source

```tsx title="components/description-list.tsx"
"use client"

import * as React from "react"
import { Check, Copy, ExternalLink } from "lucide-react"

import { cn } from "@/lib/utils"

export type DescriptionBadgeTone = "success" | "warning" | "danger" | "info" | "violet" | "neutral"

export interface DescriptionItem {
  label: string
  /** Leave empty (null, undefined or "") to show the placeholder */
  value?: React.ReactNode
  /** Show a copy button next to the value */
  copyable?: boolean
  /** Text placed on the clipboard; defaults to the value when it is a string or number */
  copyText?: string
  /** Render the value as a link */
  href?: string
  /** Render the value as a status badge */
  badge?: DescriptionBadgeTone
  /** Monospace value, good for ids and keys */
  mono?: boolean
  /** In the grid layout, span both columns */
  wide?: boolean
}

export interface DescriptionListProps extends Omit<React.ComponentProps<"dl">, "children"> {
  items: DescriptionItem[]
  layout?: "vertical" | "horizontal" | "grid"
  /** Text shown for empty values */
  emptyText?: string
}

const badgeTone: Record<DescriptionBadgeTone, { box: string; dot: string }> = {
  success: { box: "bg-emerald-500/10 text-emerald-700 ring-emerald-500/25 dark:text-emerald-300", dot: "bg-emerald-500" },
  warning: { box: "bg-amber-500/10 text-amber-700 ring-amber-500/25 dark:text-amber-300", dot: "bg-amber-500" },
  danger: { box: "bg-rose-500/10 text-rose-700 ring-rose-500/25 dark:text-rose-300", dot: "bg-rose-500" },
  info: { box: "bg-sky-500/10 text-sky-700 ring-sky-500/25 dark:text-sky-300", dot: "bg-sky-500" },
  violet: { box: "bg-violet-500/10 text-violet-700 ring-violet-500/25 dark:text-violet-300", dot: "bg-violet-500" },
  neutral: { box: "bg-muted text-muted-foreground ring-border", dot: "bg-muted-foreground" },
}

function CopyButton({ text, label }: { text: string; label: string }) {
  const [copied, setCopied] = React.useState(false)
  const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null)

  React.useEffect(() => {
    return () => {
      if (timer.current) clearTimeout(timer.current)
    }
  }, [])

  async function copy() {
    try {
      await navigator.clipboard.writeText(text)
      setCopied(true)
      if (timer.current) clearTimeout(timer.current)
      timer.current = setTimeout(() => setCopied(false), 1500)
    } catch {
      // Clipboard unavailable (insecure context or denied permission); fail silently.
    }
  }

  return (
    <button
      type="button"
      onClick={copy}
      aria-label={copied ? `${label} copied` : `Copy ${label}`}
      className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md px-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"
    >
      {copied ? <Check className="size-3.5 text-emerald-500" aria-hidden /> : <Copy className="size-3.5" aria-hidden />}
      <span aria-live="polite">{copied ? "Copied" : ""}</span>
    </button>
  )
}

function isEmpty(value: React.ReactNode) {
  return value === null || value === undefined || value === "" || value === false
}

function ItemValue({ item, emptyText }: { item: DescriptionItem; emptyText: string }) {
  if (isEmpty(item.value)) {
    return <span className="text-muted-foreground italic">{emptyText}</span>
  }

  const external = item.href ? /^https?:\/\//.test(item.href) : false
  let content: React.ReactNode = item.value
  if (item.badge) {
    const tone = badgeTone[item.badge]
    content = (
      <span className={cn("inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset", tone.box)}>
        <span className={cn("size-1.5 rounded-full", tone.dot)} aria-hidden />
        {item.value}
      </span>
    )
  } else if (item.href) {
    content = (
      <a
        href={item.href}
        {...(external ? { target: "_blank", rel: "noreferrer noopener" } : {})}
        className="inline-flex items-center gap-1 rounded-sm font-medium text-primary underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"
      >
        <span className="[overflow-wrap:anywhere]">{item.value}</span>
        {external && (
          <>
            <ExternalLink className="size-3.5 shrink-0" aria-hidden />
            <span className="sr-only">(opens in a new tab)</span>
          </>
        )}
      </a>
    )
  }

  const copyText = item.copyText ?? (typeof item.value === "string" || typeof item.value === "number" ? String(item.value) : undefined)

  return (
    <>
      <span className={cn("min-w-0 [overflow-wrap:anywhere]", item.mono && "font-mono text-[13px]")}>{content}</span>
      {item.copyable && copyText !== undefined && <CopyButton text={copyText} label={item.label} />}
    </>
  )
}

export function DescriptionList({ items, layout = "vertical", emptyText = "Not provided", className, ...props }: DescriptionListProps) {
  const horizontal = layout === "horizontal"
  return (
    <dl
      className={cn(
        "m-0 w-full min-w-0",
        layout === "vertical" && "grid gap-4",
        horizontal && "divide-y",
        layout === "grid" && "grid gap-x-8 gap-y-5 sm:grid-cols-2",
        className
      )}
      {...props}
    >
      {items.map((item) => (
        <div
          key={item.label}
          className={cn(
            "min-w-0",
            horizontal && "grid gap-1 py-3 first:pt-0 last:pb-0 sm:grid-cols-[minmax(7rem,10rem)_minmax(0,1fr)] sm:items-center sm:gap-4",
            layout === "grid" && item.wide && "sm:col-span-2"
          )}
        >
          <dt className={cn("text-muted-foreground", horizontal ? "text-sm" : "mb-1 text-xs font-medium tracking-wide uppercase")}>{item.label}</dt>
          <dd className="m-0 flex min-w-0 items-center gap-2 text-sm text-foreground">
            <ItemValue item={item} emptyText={emptyText} />
          </dd>
        </div>
      ))}
    </dl>
  )
}
```
