Description list
FreeA record's details as label and value pairs, side by side when there is room, with copy and edit where the value sits.
pnpm dlx shadcn@latest add https://caliperui.site/r/description-list.jsonCustomer
- Name
- Northwind Trading
- Billing email
- billing@northwind.exampleInvoices and receipts go here
- Customer ID
- cus_9fQ2Lx81Zr
- Plan
- Growth Active
- Balance
- €1,240.00Due 31 Oct
- Tax ID
- Not set
- Created
- 14 March 2024
- Billing address
- Rua do Ouro 120, 1100-063 Lisboa, Portugal
When to use #
Use it for
- Detail panels for a customer, an order or a resource.
- Settings summaries people read more than change.
Skip it when
- Many records to compare; use a table.
- Long forms; use fields.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/description-list.json
Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useState } from "react" import { Badge } from "@/components/ui/badge" import { DescriptionList } from "@/components/ui/description-list" import { Switch } from "@/components/ui/switch" const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)); export function DescriptionListDemo() { const [name, setName] = useState("Northwind Trading"); const [email, setEmail] = useState("billing@northwind.example"); const [loading, setLoading] = useState(false); return ( <div className="grid w-full max-w-3xl gap-4"> <label className="flex items-center gap-2 text-[13px] text-muted-foreground"> <Switch checked={loading} onChange={(event) => setLoading(event.target.checked)} /> Show loading state </label> <DescriptionList title="Customer" columns={2} loading={loading} items={[ { term: "Name", value: name, edit: { value: name, onSave: setName } }, { term: "Billing email", value: email, hint: "Invoices and receipts go here", edit: { value: email, type: "email", onSave: async (next) => { await wait(500); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(next)) throw new Error("Enter an email address like name@company.com."); if (next.startsWith("taken@")) throw new Error("Another customer already uses that address."); setEmail(next); }, }, }, { term: "Customer ID", value: "cus_9fQ2Lx81Zr", mono: true, copy: "cus_9fQ2Lx81Zr" }, { term: "Plan", value: <span className="inline-flex items-center gap-2">Growth <Badge variant="success">Active</Badge></span> }, { term: "Balance", value: "€1,240.00", mono: true, hint: "Due 31 Oct" }, { term: "Tax ID" }, { term: "Created", value: "14 March 2024" }, { term: "Billing address", value: "Rua do Ouro 120, 1100-063 Lisboa, Portugal" }, ]} /> </div> ); }
API reference #
DescriptionList
component
Accessibility #
It exposes alert semantics, with aria-labelledby, aria-busy, aria-hidden, aria-label, aria-invalid and aria-describedby kept in step with state. Changes that matter are announced through a live region.
Motion #
Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.