# Details list

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

- Registry name: `details-list` (`@candor/details-list`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/details-list

## Install

```bash
npx candor-ui add details-list
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/details-list`.
It brings in: collapsible, heading-level, heading-scope, locale, motion, tokens, use-copy, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"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>

Props:

- `groups: 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` (default `"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`

## Strings (`detailsList`)

The fixed words, English by default. Override any of them under `detailsList` in `<LocaleProvider strings={{ detailsList: { … } }}>`; the table is exported as `detailsListStrings`.

- `empty`: No details.
- `notKnown`: Not known
- `copy`: Copy {label}
- `copied`: Copied
- `failed`: Couldn’t copy

## 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).

```ts
"text" | "command" | "code" | "link"
```

### DetailRow

One label and its value.

```ts
{
  /** 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).

```ts
"rows" | "grid"
```

### DetailGroup

A titled group of rows.

```ts
{
  /** 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

Import: `import type { HeadingLevel } from "@/lib/candor/heading-level"` (item `heading-level`, https://candor.themethodic.com/docs/heading-level.md)

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.

```ts
1 | 2 | 3 | 4 | 5 | 6
```

## Motion

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

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
