rapidUI

Description List

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

Installs to @/components/ui/description-list.

Manual installation

Copy the source into your project:

components/ui/description-list.tsx
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 (a div around one pair), DescriptionListTerm (dt) and DescriptionListDetails (dd)
  • Two layouts: vertical stacks the term above the value, horizontal puts 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-slot attributes 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

PropTypeDefaultDescription
orientation'vertical' | 'horizontal''vertical'Stack the term above the value, or put the terms beside the values
dividedbooleanfalseDraw a rule between the pairs
columns1 | 2 | 3 | 41The largest number of pairs per row

CSS variables

Set these on DescriptionList with className, for example [--description-list-term-width:--spacing(28)].

VariableDefaultDescription
--description-list-term-width10remWidth of the term column in the horizontal layout
--description-list-details-min10remIn the horizontal layout, the value moves below the term when it would get narrower than this
--description-list-column-min16remThe narrowest a grid column may get before the list drops a column
--description-list-gap1.5remSpace 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: the dl. Takes orientation, divided, columns and all dl attributes
  • DescriptionListItem: a div that wraps one term and its value (or several dt/dd elements that belong together)
  • DescriptionListTerm: the term (dt), in the muted text colour
  • DescriptionListDetails: the value (dd)

Accessibility

  • The dl, dt and dd elements give screen readers the relationship between each term and its value. Wrap each pair in DescriptionListItem: it is a div, which the HTML standard allows inside a dl
  • Don't put other elements (icons, separators) directly in DescriptionListItem. Put them inside the term or the value, and hide decorative icons with aria-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 a role="status" region
  • Both layouts reflow at narrow widths and 400% zoom without horizontal scrolling
  • 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