Skip to contentVibraUI
Data display

Description list

Term and value pairs in one, two, or three columns — the panel a detail page puts its facts in.

Server-compatible: no hooks, no client boundary. A real <dl>, with each pair wrapped in a div so a grid can lay several pairs out at once. Orientation is applied through the pairs rather than the list, so a hand-composed child that carries data-slot="description-list-item" lines up with the ones built from items — pass children instead of items whenever a value needs its own markup. Terms are muted and values are not, so a column of values reads as the content and the labels stay out of the way. Columns step down to one on small screens.

Install

npx shadcn@latest add @vibra/description-list

Needs the @vibra registry in your components.json — set it up once.

Examples

Props

PropTypeDefaultDescription
items{ term: React.ReactNode; description: React.ReactNode }[]—The pairs to render. Leave it unset and hand-compose the children instead.
columns1 | 2 | 31Pairs per row at the widest breakpoint; 3 steps down to 2 and then to 1.
orientation"horizontal" | "vertical""horizontal"horizontal puts the term in a fixed 8rem column; vertical stacks it above the value.
size"sm" | "default""default"sm drops the text to text-xs and tightens the row gaps.
classNamestring—Merged onto the dl root; the remaining dl props are spread onto it too.
DescriptionTermReact.ComponentProps<"dt">—The quieter half of a pair, for hand-composed children.
DescriptionDetailsReact.ComponentProps<"dd">—The value itself, wrapping rather than overflowing its column.

Dependencies

Source

components/ui/description-list.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

// Orientation is applied to the pairs rather than to the <dl>, so a
// hand-composed child that carries the same data-slot lines up with the ones
// built from `items`.
const descriptionListVariants = cva("grid", {
  variants: {
    columns: {
      1: "grid-cols-1",
      2: "grid-cols-1 sm:grid-cols-2",
      3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
    },
    orientation: {
      horizontal:
        "*:data-[slot=description-list-item]:grid *:data-[slot=description-list-item]:grid-cols-[minmax(0,8rem)_minmax(0,1fr)] *:data-[slot=description-list-item]:items-baseline",
      vertical: "*:data-[slot=description-list-item]:flex *:data-[slot=description-list-item]:flex-col",
    },
    size: {
      default: "gap-x-6 gap-y-4 text-sm *:data-[slot=description-list-item]:gap-x-4 *:data-[slot=description-list-item]:gap-y-0.5",
      sm: "gap-x-5 gap-y-3 text-xs *:data-[slot=description-list-item]:gap-x-3 *:data-[slot=description-list-item]:gap-y-0.5",
    },
  },
  defaultVariants: { columns: 1, orientation: "horizontal", size: "default" },
})

export type DescriptionListItem = {
  term: React.ReactNode
  description: React.ReactNode
}

export type DescriptionListProps = React.ComponentProps<"dl"> & {
  /** The pairs to render. Leave it unset and hand-compose the children instead. */
  items?: DescriptionListItem[]
  columns?: NonNullable<VariantProps<typeof descriptionListVariants>["columns"]>
  orientation?: NonNullable<VariantProps<typeof descriptionListVariants>["orientation"]>
  size?: NonNullable<VariantProps<typeof descriptionListVariants>["size"]>
}

/** A definition list of term/value pairs — the panel a detail page puts its facts in. */
function DescriptionList({
  className,
  items,
  columns = 1,
  orientation = "horizontal",
  size = "default",
  children,
  ...props
}: DescriptionListProps) {
  return (
    <dl
      data-slot="description-list"
      data-columns={columns}
      data-orientation={orientation}
      data-size={size}
      className={cn(descriptionListVariants({ columns, orientation, size }), className)}
      {...props}
    >
      {items?.map((item, index) => (
        <div key={index} data-slot="description-list-item">
          <DescriptionTerm>{item.term}</DescriptionTerm>
          <DescriptionDetails>{item.description}</DescriptionDetails>
        </div>
      ))}
      {children}
    </dl>
  )
}

/** The quieter half of a pair: what the value beside it is. */
function DescriptionTerm({ className, ...props }: React.ComponentProps<"dt">) {
  return (
    <dt
      data-slot="description-term"
      className={cn("min-w-0 text-muted-foreground", className)}
      {...props}
    />
  )
}

/** The value itself, wrapping rather than overflowing its column. */
function DescriptionDetails({ className, ...props }: React.ComponentProps<"dd">) {
  return (
    <dd
      data-slot="description-details"
      className={cn("min-w-0 break-words text-foreground", className)}
      {...props}
    />
  )
}

export { DescriptionList, DescriptionDetails, DescriptionTerm, descriptionListVariants }