Details list
ProTitled groups of label-and-value rows for a card where a person reads and confirms how something will be done: the commands that will run, the checks and their time limits, the services and their ports, the paths that carry risk. Each group is a real description list (dl, dt, dd), so a screen reader reads it as one, and its heading is a button that folds the rows away (aria-expanded), with an optional summary shown while it is folded. A folded group's rows stay in the page, hidden until found, so the browser's find-in-page can reach them and opens the group when it lands on one. A command value is monospace, wraps, and has a copy control that says "Copied" only once the clipboard took it. Nothing is hidden from the person who has to confirm: groups start open unless you say otherwise, and an unknown value shows as "Not known". A group the person must read before confirming stays open: leave its defaultOpen alone, or set collapsible to false so it can't be folded.
layout="grid" lays each group's rows out as cells, the label over the value, in as many columns as the list's width holds, with hairlines between them: a fact sheet (an environment's ID, version, edition and last sync). The terms and values are the same elements in the same order in both layouts, so they read the same. A "code" value is an identifier: monospace, left to right in any page, and never translated; give a row copy to add a copy control.
AttributesRail is the flat sibling, one list of facts about a record with no groups, folding or commands. It draws no border or padding of its own, so it sits in a Surface or a card.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
- 5.1.0
- 5.0.0
- 4.5.0
- 4.4.0
- 4.3.0
- 4.2.0
- 4.1.0
- 4.0.2
- 4.0.1
- 4.0.0
- 3.0.0
- 2.10.0
- 2.9.0
- 2.8.0
- 2.7.0
- 2.6.2
- 2.6.1
- 2.6.0
- 2.5.1
- 2.5.0
- 2.4.0
- 2.3.0
- 2.2.0
- 2.1.0
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1 and 2.0.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/details-list
Brings in: bidi, collapsible, heading-level, heading-scope, link, locale, motion, tokens, use-copy, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { DetailsList } from "@/components/candor/details-list"
export default function Example() {
return (
<div className="flex max-w-3xl flex-col gap-6">
<div className="max-w-xl rounded-xl border bg-card p-4">
<p className="text-sm font-semibold">How the billing service will be built and checked</p>
<p className="mt-0.5 text-xs text-muted-foreground">Read each part, then confirm below.</p>
<DetailsList
className="mt-2"
groups={[
{
id: "install",
title: "Install",
summary: "2 commands",
rows: [
{ label: "Dependencies", value: "npm ci", kind: "command" },
{ label: "Database", value: "npm run db:migrate -- --env staging", kind: "command" },
],
},
{
id: "checks",
title: "Checks",
summary: "3 checks, 2 with time limits",
rows: [
{ label: "Unit tests", value: "npm test — stops after 5 minutes" },
{ label: "Types", value: "npm run typecheck — stops after 2 minutes" },
{ label: "Lint", value: "npm run lint — no time limit" },
],
},
{
id: "services",
title: "Services",
summary: "1 service",
defaultOpen: false, // Folded only to show `summary`; a group the person must read before confirming stays open.
rows: [
{ label: "api", value: "registry.example.com/billing-api:4.2 on port 8080", kind: "code" },
{ label: "Health check", value: "GET /healthz every 10 seconds" },
{ label: "Runbook", value: "Billing on-call", kind: "link", href: "/runbooks/billing" },
],
},
{
id: "risk",
title: "Risky paths",
summary: "2 paths",
rows: [
{ label: "Writes to", value: "migrations/, db/seed.sql", kind: "code" },
{ label: "Rollback", value: null },
],
},
]}
/>
</div>
{/* The same list as a fact sheet: each fact a cell, its label over its value, as many columns as the card holds. */}
<div className="rounded-xl border bg-card p-4">
<DetailsList
layout="grid"
groups={[
{
id: "environment",
title: "Production environment",
collapsible: false,
rows: [
{ label: "Environment ID", value: "env_7f3a9c21d04b", kind: "code", copy: true },
{ label: "API version", value: "62.0", kind: "code" },
{ label: "Edition", value: "Enterprise" },
{ label: "Instance", value: "eu-west-2" },
{ label: "Items", value: "1,667" },
{ label: "Last full sync", value: "12 November 2026, 06:00" },
],
},
]}
/>
</div>
</div>
)
}<DetailsList>
| Prop | Type | Default | Description |
|---|---|---|---|
| groupsrequired | DetailGroup[] | — | The groups, in order. |
| headingLevel | HeadingLevel | undefined | — | The heading level of each group's title, so the card fits the page's outline. Defaults to one below the nearest `HeadingScope`, else 2 (`<h2>`). |
| layout | DetailsLayout | undefined | "rows" | `"rows"` (the default) puts each label beside its value; `"grid"` makes each row a cell, its label over its value, in columns that follow the list's own width. |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under detailsList in <LocaleProvider strings={{ … }}>; the table is exported as detailsListStrings.
| Key | English |
|---|---|
| empty | No details. |
| notKnown | Not known |
| copy | Copy {label} |
| copied | Copied |
| failed | Couldn’t copy |
Motion
| Moment | Family | What it does |
|---|---|---|
| open | reveal, size | The chevron beside a group turns when the person opens it and turns back when they close it. Under reduced motion it turns at once. |
| tick | pop, scale | After the person copies a command, a tick (or a cross, if the browser refused the clipboard) pops in on the button once the browser has answered. Nothing shows before the answer, and under reduced motion it fades in. |
Set with --cd-motion-details-list-<moment>-* in your brand file; the families are in the design reference.
Types
- DetailKind
- How a row's value is shown: "text" (the default), "command" (monospace, wraps, with a copy control), "code" (monospace, no control) or "link" (inside the app only).
"text" | "command" | "code" | "link"- DetailRow
- One label and its value.
{ /** What the value is ("Install", "Port"). It is the row's term, and names the copy control. */ label: string; /** The value, as text (never HTML). A missing, null, empty or blank value shows as "Not known". */ value: string | null | undefined; /** How the value is shown. Defaults to `"text"`. */ kind?: DetailKind | undefined; /** Where a `"link"` row goes. Only an address inside the app is followed (starting `/`, `#` or `?`, not `//`); anything else shows as plain text. */ href?: string | undefined; /** Adds a copy control after a `"text"` or `"code"` value, named "Copy {label}", which says "Copied" only once the clipboard took the value (an ID a person pastes elsewhere). A `"command"` row has its own; a link or a missing value has none. */ copy?: boolean | undefined; }- DetailsLayout
- How a list lays out its rows: `"rows"` (label beside value, the default) or `"grid"` (each row a cell, label over value, in columns that follow the list's width).
"rows" | "grid"- DetailGroup
- A titled group of rows.
{ /** Stable key for the group. */ id: string; /** The group's heading. */ title: string; /** The group's rows, in order. */ rows: DetailRow[]; /** Whether the heading folds the rows away. Defaults to true. */ collapsible?: boolean | undefined; /** Whether a collapsible group starts open. Defaults to true. */ defaultOpen?: boolean | undefined; /** A line shown beside the heading while the group is folded, so it can be skimmed shut ("3 checks, 2 with time limits"). */ summary?: string | undefined; }
Types from other items
The props above take these. Each is defined by another item; import it from the path shown.
- HeadingLevel from @/lib/candor/heading-level
- A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6