DataList
The read-only 'field: value' detail block for one entity — a real description list (<dl>/<dt>/<dd>) in three orientations.
@garn/ui/data-listDefault
The default row layout — four field/value pairs.
Orientations
row · stacked · grid (aligned) side by side.
Dividers
Hairline rules between rows, with Badge / Avatar values.
Copyable
Inline copy affordances on IDs / keys / emails.
Empty
Empty values — the default em-dash and a custom placeholder.
Sizes
sm · md · lg.
Overflow
Two long-value strategies — truncate (one line + title) vs wrap (default).
In Card
A patient detail panel inside a Card with a status Tag.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add data-list- Registry
data-list- Deps
class-variance-authority@radix-ui/react-slotlucide-react- Registry deps
utilsuse-clipboarduse-announcebutton
Import
import { DataList.Item, DataList.Term, DataList.Value, DataList.CopyButton } from "@garn/ui/data-list";Anatomy
The parts this component is built from — compose them to assemble it.
DataList.ItemRequired[data-slot="data-list-item"]One term/value pair — a <div> inside the <dl> (valid HTML, the layout host). In grid it spans both columns via subgrid so every value edge aligns; carries the optional divider.
DataList.TermRequired[data-slot="data-list-term"]The term (<dt>) — the field name. Muted; holds the label column (floored, not fixed, so a long term grows past the floor). asChild to become a heading/link.
DataList.ValueRequired[data-slot="data-list-value"]The value (<dd>) — any node. Empty → tokenized placeholder + sr-only 'Not set'. Opt-in truncate (title tooltip) and copyable. asChild to become a link.
DataList.CopyButton[data-slot="data-list-copy"]The inline copy affordance — a ghost Button that writes to the clipboard, flips to a check for a beat, and announces 'Copied' via a polite live region. Auto-rendered by copyable.
Props
The component's public props and their types.
orientation- Description
- No description.
- Type
"grid" | "row" | "stacked"- Default
row
size- Description
- No description.
- Type
"lg" | "md" | "sm"- Default
md
align- Description
- Cross-axis alignment of term↔value (`row`/`grid`) — `baseline` sits a Tag/Badge value on the term's text baseline; `start`/`center` align on the box.
- Type
"baseline" | "center" | "start"- Default
"baseline"
density- Description
- Local density override; omit to inherit the ambient `[data-density]`.
- Type
"compact" | "spacious"
divider- Description
- Hairline rule between items (never above the first / below the last).
- Type
boolean- Default
false
labelWidth- Description
- Width of the term ("label") column in `row`/`grid`. Pass a token-backed length (e.g. a CSS var or a Tailwind width via className) or a number of px; omit for the natural `max-content` width. Sets `--data-list-label-w`.
- Type
number | string
placeholder- Description
- Rendered for an empty value; `aria-hidden` with an sr-only "Not set" so the absence is meaningful to assistive tech.
- Type
React.ReactNode- Default
"—"
truncate- Description
- Ambient truncation for every value (a value can still override).
- Type
boolean- Default
false
Plus 280 inherited native <dl> attributes.
DataList.Item
align- Description
- Override the root's cross-axis alignment for this row.
- Type
"baseline" | "center" | "start"
Plus 280 inherited native HTML attributes.
DataList.Term
asChild- Description
- Render as the child element (e.g. a heading or link), merging props.
- Type
boolean- Default
false
Plus 280 inherited native HTML attributes.
DataList.Value
asChild- Description
- Render as the child element (e.g. a link), merging props.
- Type
boolean- Default
false
copyable- Description
- Show an inline copy affordance (a ghost Button) on hover / focus-within.
- Type
boolean- Default
false
copyText- Description
- Text to copy when `copyable`; defaults to the value's rendered text.
- Type
string
placeholder- Description
- Override the root's empty placeholder for this value.
- Type
React.ReactNode
truncate- Description
- Truncate a long value to one line with an ellipsis + native title tooltip. Overrides the root's ambient `truncate`.
- Type
boolean
Plus 280 inherited native HTML attributes.
DataList.CopyButton
className- Description
- No description.
- Type
string
label- Description
- Accessible label for the copy button.
- Type
string- Default
"Copy value"
valueRequired- Description
- The text written to the clipboard.
- Type
string
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="data-list"]States
| Selector | State | Description |
|---|---|---|
[data-slot=data-list-value][data-empty] | empty-value | A value with no content — renders the tokenized placeholder (aria-hidden) + an sr-only 'Not set', holding the column. |
[data-slot=data-list-copy] | copy-confirm | After a copy, the button flips to a check for a beat and announces 'Copied' via a polite live region. |
| Property | Token | Tier |
|---|---|---|
rowGap | --garn-gap-stack | semantic · air |
columnGap | --garn-gap-stack | semantic · air |
Private instance vars (never externalize): --data-list-label-w
When to use
A read-only set of 'field: value' pairs about one entity — demographics, an order/receipt summary, settings recap, a details tab, or a metadata sidebar. Use row for the classic detail block, grid when values must align across rows, stacked for narrow asides.
Reach for something else when
- Tabular data with multiple rows × columns (use Table)
- editable fields (use Form + inputs)
- a single big metric (use Stat)
- a chronology of events (use Timeline)
- inline tokens/labels (use Badge / Tag).
Overview
DataList presents a read-only set of term→value pairs about one entity — demographics, an order summary, a settings recap, a metadata sidebar. It renders real <dl>/<dt>/<dd> (DataList.Item / Term / Value), with empty values showing a placeholder + sr-only "Not set" and opt-in truncate/copyable. Use row for a classic detail block, grid when values must align across rows, stacked for narrow asides. For tabular records across many entities, use a Table.
Guidelines
Use it for one entity's facts, not a data grid. DataList describes a single thing (a user, an order); comparing many rows of records is a Table's job. Keep terms short and consistent, and order them by importance.
Show empty values honestly. Render a clear "not set" placeholder rather than collapsing the row, so the absence is itself information; the component announces it to screen readers. Make long values truncate with a tooltip rather than breaking the layout.
Pick the layout for the content. grid when value alignment aids scanning, row for prose-y detail, stacked for narrow columns. Use copyable on the values people actually copy (IDs, emails) and keep the markup semantic so AT reads it as a definition list.
Best practices
- Use the real parts (DataList.Item / .Term / .Value) so the <dl>/<dt>/<dd> semantics hold.
- Pick
grid(or setlabelWidth) when you want every value's left edge to line up. - Drop rich nodes (Tag, Badge, Avatar, a link) straight into DataList.Value — it composes with the badge family.
- Let an empty value render the placeholder (don't hide the row) so the field still reads as 'not set'.
- Use
copyablefor IDs / keys / emails the user will copy;truncatefor long single-line values.
- Don't fake a description list with styled <div>s — you lose the announced term↔value relationship.
- Don't use DataList for rows×columns of records — that's Table.
- Don't hand-place a copy button; let
copyablerender and wire the announced one. - Don't hardcode the label column width in px utilities — use
labelWidth(token-backed) which feeds the var.
Content guidelines
- Keep terms short and consistent (sentence case, parallel phrasing).
- Prefer a meaningful empty placeholder ('None on file', 'Not provided') over a bare dash where it adds clarity.
Troubleshooting
Values don't line up across rows.
Cause. row floors the label column to the widest term; exact alignment needs the grid or an explicit width.
Fix. Use orientation="grid" (subgrid alignment) or set labelWidth so every value left-edge lines up.
The copy button doesn't appear / nothing happens on copy.
Cause. Copy is gated by the copyable prop on DataList.Value, and the clipboard can reject in an insecure context.
Fix. Set copyable on the Value (the button auto-renders); serve over https so navigator.clipboard resolves.
An empty field collapses the row or shows nothing.
Cause. An empty value still renders a placeholder by design (holding the column).
Fix. Let it render — pass a clearer placeholder ('None on file') if the dash is ambiguous.
Building this with <div>s and CSS for the label/value look.
Cause. You lose the announced term↔value relationship that <dl>/<dt>/<dd> gives.
Fix. Use DataList.Item / .Term / .Value — the semantics are the whole point.
Accessibility
- Role
term- Focus
- Only the copy button and any asChild link/heading are focusable, each with a 2px --garn-ring focus ring.
- Real <dl> / <dt> / <dd> semantics so the term↔value association is announced, not just visually implied — never fake it with styled <div>s.
- A DataList.Item is a <div> wrapper (valid since HTML5.2); in `grid` it adopts the parent columns via subgrid so alignment never needs per-cell borders.
- An empty value renders a tokenized, aria-hidden placeholder PLUS an sr-only 'Not set', so the absence is meaningful to assistive tech and the column still holds.
- The copy affordance is a real focusable <button> (reachable by keyboard, visible on touch) that announces 'Copied' via a role=status polite live region.
- Use asChild on Term/Value to host a heading or link without breaking the <dt>/<dd> semantics.