Skip to content

DataList

The read-only 'field: value' detail block for one entity — a real description list (<dl>/<dt>/<dd>) in three orientations.

Stablev0.4.2added in v0.1.0@garn/ui/data-list
On this page

Default

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.

terminal
npx garn-ui add data-list
First time? Set up garn in your project
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

SelectorState
[data-slot=data-list-value][data-empty]empty-value
[data-slot=data-list-copy]copy-confirm
PropertyTokenTier
rowGap--garn-gap-stacksemantic · air
columnGap--garn-gap-stacksemantic · 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

Do
  • Use the real parts (DataList.Item / .Term / .Value) so the <dl>/<dt>/<dd> semantics hold.
  • Pick grid (or set labelWidth) 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 copyable for IDs / keys / emails the user will copy; truncate for long single-line values.
Don't
  • 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 copyable render 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.
A1.3.1Info and RelationshipsAA1.4.3Contrast (Minimum)AA2.4.7Focus VisibleA4.1.2Name, Role, Value
  • 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.