Key–value pairs such as applicant, property and fee, as valid dl markup in stacked, side-by-side or grid layouts.
Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/description-list.jsonInstalls to @/components/ui/description-list.
Manual installation
Copy the source into your project:
import type * as React from 'react'
import { cn } from '@/lib/utils'
// The layout is driven by data attributes on the <dl>, so the parts need no React context and work in server components.
// The grid of pairs responds to the list's own width, not the viewport, so it also behaves inside cards and side panels:
// it shows up to `columns` columns, and drops to fewer when a column would be narrower than --description-list-column-min.
function DescriptionList({
className,
orientation = 'vertical',
divided = false,
columns = 1,
...props
}: React.ComponentProps<'dl'> & {
/** `vertical` stacks the term above the value; `horizontal` puts terms in a column beside the values */
orientation?: 'vertical' | 'horizontal'
/** Draw a rule between the pairs */
divided?: boolean
/** The largest number of pairs per row; fewer are shown when the list is too narrow */
columns?: 1 | 2 | 3 | 4
}) {
return (
<dl
data-slot="description-list"
data-orientation={orientation}
data-divided={divided ? '' : undefined}
data-columns={columns}
className={cn(
'group/description-list grid w-full grid-cols-[repeat(auto-fill,minmax(min(100%,max(var(--description-list-column-min),calc((100%_-_(var(--description-list-columns)_-_1)_*_var(--description-list-gap))_/_var(--description-list-columns)))),1fr))] gap-x-(--description-list-gap) gap-y-4 text-sm [--description-list-column-min:--spacing(64)] [--description-list-columns:1] [--description-list-gap:--spacing(6)] [--description-list-details-min:--spacing(40)] [--description-list-term-width:--spacing(40)] data-[columns=2]:[--description-list-columns:2] data-[columns=3]:[--description-list-columns:3] data-[columns=4]:[--description-list-columns:4]',
// Dividers: every pair gets a top rule and the clip-path hides the rules of the first row, whatever the number
// of columns. The clip only trims the top edge, so focus rings at the sides and bottom stay visible.
'data-divided:gap-y-0 data-divided:[clip-path:inset(1px_-1rem_-1rem_-1rem)]',
className,
)}
{...props}
/>
)
}
function DescriptionListItem({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="description-list-item"
className={cn(
'flex min-w-0 flex-col gap-1',
// Horizontal: the term has a fixed width and the value takes the rest. When the value would get narrower than
// --description-list-details-min (10rem) it wraps below the term, so the pair stays readable on small screens
// and in narrow panels.
'group-data-[orientation=horizontal]/description-list:flex-row group-data-[orientation=horizontal]/description-list:flex-wrap group-data-[orientation=horizontal]/description-list:items-baseline group-data-[orientation=horizontal]/description-list:gap-x-4',
'group-data-divided/description-list:border-t group-data-divided/description-list:py-3',
className,
)}
{...props}
/>
)
}
function DescriptionListTerm({ className, ...props }: React.ComponentProps<'dt'>) {
return (
<dt
data-slot="description-list-term"
className={cn(
'min-w-0 text-muted-foreground wrap-break-word group-data-[orientation=horizontal]/description-list:w-(--description-list-term-width) group-data-[orientation=horizontal]/description-list:max-w-full group-data-[orientation=horizontal]/description-list:shrink-0',
className,
)}
{...props}
/>
)
}
function DescriptionListDetails({ className, ...props }: React.ComponentProps<'dd'>) {
return (
<dd
data-slot="description-list-details"
className={cn(
'min-w-0 text-foreground wrap-break-word group-data-[orientation=horizontal]/description-list:min-w-[min(100%,var(--description-list-details-min))] group-data-[orientation=horizontal]/description-list:flex-1 [&_a:not([data-slot])]:underline [&_a:not([data-slot])]:underline-offset-4 [&_a:not([data-slot])]:hover:text-primary',
className,
)}
{...props}
/>
)
}
export { DescriptionList, DescriptionListItem, DescriptionListTerm, DescriptionListDetails }Key–value pairs such as applicant, property and fee. Almost every case view has them. The component renders valid
<dl> markup, so screen readers announce each term with its value.
Features
- Valid
<dl>markup:DescriptionList(dl),DescriptionListItem(adivaround one pair),DescriptionListTerm(dt) andDescriptionListDetails(dd) - Two layouts:
verticalstacks the term above the value,horizontalputs terms in a column beside the values - In the horizontal layout the value moves below the term when there is not enough room for both
- Optional dividers between pairs with
divided - A responsive grid of pairs with
columns. It responds to the list's own width, so it also works in cards and side panels - Long values wrap, even long email addresses and URLs
- Values can hold links, badges and buttons. Plain links are underlined automatically
data-slotattributes on every part
Usage
import {
DescriptionList,
DescriptionListDetails,
DescriptionListItem,
DescriptionListTerm,
} from '@/components/ui/description-list'
export function CaseFacts() {
return (
<DescriptionList orientation="horizontal" divided>
<DescriptionListItem>
<DescriptionListTerm>Applicant</DescriptionListTerm>
<DescriptionListDetails>Anna Lindqvist</DescriptionListDetails>
</DescriptionListItem>
<DescriptionListItem>
<DescriptionListTerm>Property</DescriptionListTerm>
<DescriptionListDetails>Björken 4:12</DescriptionListDetails>
</DescriptionListItem>
<DescriptionListItem>
<DescriptionListTerm>Fee</DescriptionListTerm>
<DescriptionListDetails>14 820 kr</DescriptionListDetails>
</DescriptionListItem>
</DescriptionList>
)
}Examples
Default
The facts of a case, stacked: applicant, property, received, handler and fee.
Horizontal
Terms in a column beside the values, with dividers. Long values wrap in their own column. Make the list narrower and each value moves below its term.
In a Card
The usual case view: a card with the case facts. Set --description-list-term-width to fit the term column to your
terms.
Rich Values
Values can hold a status badge, a link and a copy button for the case number. Make the value a flex row
(className="flex items-center gap-1") when it holds a button. Plain links are underlined, so they are not told apart by
colour alone.
Two-Column Grid
columns={2} shows two pairs per row when the list is wide enough. When a column would get narrower than
--description-list-column-min (16rem), the list drops to one column.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'vertical' | 'horizontal' | 'vertical' | Stack the term above the value, or put the terms beside the values |
divided | boolean | false | Draw a rule between the pairs |
columns | 1 | 2 | 3 | 4 | 1 | The largest number of pairs per row |
CSS variables
Set these on DescriptionList with className, for example [--description-list-term-width:--spacing(28)].
| Variable | Default | Description |
|---|---|---|
--description-list-term-width | 10rem | Width of the term column in the horizontal layout |
--description-list-details-min | 10rem | In the horizontal layout, the value moves below the term when it would get narrower than this |
--description-list-column-min | 16rem | The narrowest a grid column may get before the list drops a column |
--description-list-gap | 1.5rem | Space between the columns of the grid |
With divided, the list clips its top edge to hide the rule above the first row. To frame the list with a border or
ring, put the frame on a wrapper, or use divide-y on a one-column list instead.
Subcomponents
DescriptionList: thedl. Takesorientation,divided,columnsand alldlattributesDescriptionListItem: adivthat wraps one term and its value (or severaldt/ddelements that belong together)DescriptionListTerm: the term (dt), in the muted text colourDescriptionListDetails: the value (dd)
Accessibility
- The
dl,dtandddelements give screen readers the relationship between each term and its value. Wrap each pair inDescriptionListItem: it is adiv, which the HTML standard allows inside adl - Don't put other elements (icons, separators) directly in
DescriptionListItem. Put them inside the term or the value, and hide decorative icons witharia-hidden="true" - If the term is shown only as an icon, keep a text term for screen readers:
<DescriptionListTerm><PhoneIcon aria-hidden="true" /><span className="sr-only">Phone</span></DescriptionListTerm> - Plain links in a value are underlined, so they don't rely on colour alone
- Give icon-only buttons in a value, such as a copy button, an
aria-label, and announce the result (for example "Case number copied") in arole="status"region - Both layouts reflow at narrow widths and 400% zoom without horizontal scrolling
Related Components
- Card: the usual container for case facts
- Badge: status values
- Table: many records with the same fields
- Item: lists of things with a title, description and actions