Patterns
Description List
A list of read-only facts about one record, as labels and values.
Patterns
A list of read-only facts about one record, as labels and values.
import { DescriptionItem, DescriptionList, StatusBadge } from 'ferry-ui'
export default function DescriptionListHero() {
return (
<DescriptionList aria-label="Order details">
<DescriptionItem label="Order" mono>
ORD-58213
</DescriptionItem>
<DescriptionItem label="Status">
<StatusBadge tone="success" label="Fulfilled" />
</DescriptionItem>
<DescriptionItem label="Customer">Acme</DescriptionItem>
<DescriptionItem label="Payment">Bank transfer</DescriptionItem>
<DescriptionItem label="Placed">Mar 4, 2026</DescriptionItem>
<DescriptionItem label="Shipped">Mar 5, 2026</DescriptionItem>
<DescriptionItem label="Total">
<span className="tabular">$1,284.00</span>
</DescriptionItem>
<DescriptionItem label="Tracking" mono>
TRK-0123-4567
</DescriptionItem>
</DescriptionList>
)
}Import the two parts. The list is a <dl> element. The children of an item are its value.
import { DescriptionItem, DescriptionList } from 'ferry-ui'
<DescriptionList>
<DescriptionItem label="" />
<DescriptionItem label="" />
</DescriptionList>The variant prop of the list sets the look. The items get the variant from the list.
| Variant | Look | Use |
|---|---|---|
grid (default) | A card of cells. | The facts at the top of a detail page. |
strip | One row of cells. | Short values at the bottom of a card. |
rows | Rows with the value on the right. | A side panel or the body of a card. |
inline | Two compact columns with no frame. | A popover or a menu. |
columns sets the number of columns of the grid and strip variants, from 1 to 4. The default is 4. The grid variant uses fewer columns on a small screen.
import { DescriptionItem, DescriptionList } from 'ferry-ui'
export default function DescriptionListColumns() {
return (
<div className="flex flex-col gap-6">
<DescriptionList columns={2} aria-label="Project">
<DescriptionItem label="Owner">Maya Chen</DescriptionItem>
<DescriptionItem label="Visibility">Private</DescriptionItem>
</DescriptionList>
<DescriptionList columns={3} aria-label="Subscription">
<DescriptionItem label="Plan">Business</DescriptionItem>
<DescriptionItem label="Seats">18 of 25</DescriptionItem>
<DescriptionItem label="Renews">Apr 1, 2026</DescriptionItem>
</DescriptionList>
</div>
)
}A value stays on one line. span makes a cell of the grid wider. wrap lets the text continue on more lines. mono is for an ID, a URL or a key.
import { DescriptionItem, DescriptionList } from 'ferry-ui'
const ENDPOINT = 'https://hooks.example.com/v2/workspaces/acme/integrations/billing-events/receiver'
export default function DescriptionListLongValues() {
return (
<DescriptionList aria-label="Webhook details">
<DescriptionItem label="Endpoint" mono span={2}>
<span title={ENDPOINT}>{ENDPOINT}</span>
</DescriptionItem>
<DescriptionItem label="Events">invoice.paid</DescriptionItem>
<DescriptionItem label="Last delivery">2 minutes ago</DescriptionItem>
<DescriptionItem label="Description" span="full" wrap>
Sends the paid invoices to the data warehouse of the finance team. A delivery that fails starts again for 24
hours, then the billing channel gets an alert.
</DescriptionItem>
</DescriptionList>
)
}The strip variant goes at the bottom of a Card. It always shows the number of columns that you set. Keep the values short.
import { Card, CardContent, CardDescription, CardHeader, CardTitle, DescriptionItem, DescriptionList } from 'ferry-ui'
export default function DescriptionListStrip() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<div>
<CardTitle>Design team</CardTitle>
<CardDescription>The workspace for product design.</CardDescription>
</div>
</CardHeader>
<CardContent className="text-[13px] text-foreground-light">
The brand assets and the component library are here.
</CardContent>
<DescriptionList variant="strip" columns={3} aria-label="Team details">
<DescriptionItem label="Members">12</DescriptionItem>
<DescriptionItem label="Projects">7</DescriptionItem>
<DescriptionItem label="Team ID" mono>
tm_42
</DescriptionItem>
</DescriptionList>
</Card>
)
}In the rows variant, an item can have an icon, a hint and an href. With href, the full row is a link. divided={false} removes the line above the first row.
import { Card, CardHeader, CardTitle, DescriptionItem, DescriptionList, type LinkComponent } from 'ferry-ui'
import { Globe, KeyRound, Tag, Users } from 'lucide-react'
// In an app, the link component of your router opens the page. This one stays on the page.
const DemoLink: LinkComponent = ({ href, onClick, ...props }) => (
<a
href={href}
{...props}
onClick={(event) => {
onClick?.(event)
event.preventDefault()
}}
/>
)
export default function DescriptionListRows() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Acme workspace</CardTitle>
</CardHeader>
<DescriptionList variant="rows" divided={false} aria-label="Workspace summary">
<DescriptionItem icon={<Users />} label="Members" hint="2 pending" href="#members" linkComponent={DemoLink}>
18
</DescriptionItem>
<DescriptionItem icon={<KeyRound />} label="API keys" href="#api-keys" linkComponent={DemoLink}>
4
</DescriptionItem>
<DescriptionItem icon={<Tag />} label="API version" mono>
2026-03-01
</DescriptionItem>
<DescriptionItem icon={<Globe />} label="Region" mono>
eu-west
</DescriptionItem>
</DescriptionList>
</Card>
)
}The inline variant has no frame. Use it in a Popover or in a menu.
import {
Button,
DescriptionItem,
DescriptionList,
Popover,
PopoverContent,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from 'ferry-ui'
import { Building2 } from 'lucide-react'
export default function DescriptionListInline() {
return (
<Popover>
<PopoverTrigger asChild>
<Button icon={<Building2 />}>Acme workspace</Button>
</PopoverTrigger>
<PopoverContent align="start" className="flex w-72 flex-col gap-3">
<PopoverHeader>
<PopoverTitle>Acme workspace</PopoverTitle>
</PopoverHeader>
<DescriptionList variant="inline">
<DescriptionItem label="Plan">Business</DescriptionItem>
<DescriptionItem label="Currency" mono>
EUR
</DescriptionItem>
<DescriptionItem label="Domain" mono>
acme.example.com
</DescriptionItem>
<DescriptionItem label="Owner">Maya Chen</DescriptionItem>
</DescriptionList>
</PopoverContent>
</Popover>
)
}Set loading on an item while its value loads. The item shows a skeleton and sets aria-busy.
import * as React from 'react'
import { DescriptionItem, DescriptionList, Label, Switch } from 'ferry-ui'
export default function DescriptionListLoading() {
// In an app, `loading` comes from the request that loads the record.
const [loading, setLoading] = React.useState(true)
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Switch id="customer-loading" checked={loading} onCheckedChange={setLoading} />
<Label htmlFor="customer-loading">Loading</Label>
</div>
<DescriptionList aria-label="Customer details">
<DescriptionItem label="Customer" loading={loading}>
Acme
</DescriptionItem>
<DescriptionItem label="Plan" loading={loading}>
Business
</DescriptionItem>
<DescriptionItem label="Owner" loading={loading}>
Maya Chen
</DescriptionItem>
<DescriptionItem label="Customer since" loading={loading}>
Mar 4, 2024
</DescriptionItem>
</DescriptionList>
</div>
)
}An item with no value shows a dash. The number 0 is a value.
import { DescriptionItem, DescriptionList } from 'ferry-ui'
export default function DescriptionListEmptyValues() {
return (
<DescriptionList aria-label="Task details">
<DescriptionItem label="Assignee" />
<DescriptionItem label="Due date">{null}</DescriptionItem>
<DescriptionItem label="Notes">{''}</DescriptionItem>
<DescriptionItem label="Open tasks">{0}</DescriptionItem>
</DescriptionList>
)
}title attribute.href, the label is the text of the link. The value is its description.href. The link covers the full row.DescriptionList also accepts each attribute of the <dl> element. DescriptionItem accepts each attribute of the <div> element.
| Prop | Type | Default |
|---|---|---|
variant | "grid" | "inline" | "strip" | "rows" | grid |
Look of the list (see | ||
columns | 1 | 4 | 3 | 2 | 4 |
Columns of the | ||
divided | boolean | true |
| ||
children | ReactNode | - |
| ||
| Prop | Type | Default |
|---|---|---|
labelRequired | ReactNode | - |
Short caption (1–3 words: "Status", "Issued", "Billing email"). Rendered in the | ||
children | ReactNode | - |
The value, rendered in the | ||
mono | boolean | false |
Sets the value in the monospace font at 13px: IDs, hashes, versions, URLs, keys. | ||
wrap | boolean | false |
Lets a long value wrap onto several lines instead of truncating it to one: a description, a postal address, a full URL or command. Meant for the | ||
icon | ReactNode | - |
| ||
hint | ReactNode | - |
| ||
span | 1 | 2 | "full" | 1 |
| ||
href | string | - |
| ||
linkComponent | LinkComponent | - |
Router link used when | ||
loading | boolean | false |
Shows a skeleton while the value loads and sets | ||
valueClassName | string | - |
Extra classes for the | ||