caliper uidocs

Description list

Free

A record's details as label and value pairs, side by side when there is room, with copy and edit where the value sits.

Installs components/ui/description-list.tsxDepends on @radix-ui/react-slot, lucide-reactMotion shared motion tokens
pnpm dlx shadcn@latest add https://caliperui.site/r/description-list.json

Customer

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 #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
items *readonly DescriptionItem[]—
columns1 | 21Two pairs per row on wide containers.
loadingbooleanfalse
titlestring—A small heading over the list, e.g. "Billing details".
classNamestring—

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.

KeyBehaviour
EscapeCloses or cancels, then returns focus to where it came from.

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.