GrDescriptionList
Lists label-value pairs as a real description list, values aligned in a column.
Overview
When to take it
- the card of an object — the summary of an order, a profile, a document: labels on the left, values in a column on the right;
- technical details — identifiers, hashes, MIME types, versions: a long value wraps instead of pushing the layout apart;
- some of the values are not text — a status badge, a relative time, a link are substituted with a slot on a particular pair;
- there are many pairs — up to four columns fold a long list without tearing the pairs apart;
- metadata as a caption to something else — “Messages: 3 · Created: 12.04”
inside a list item:
flowlets the pairs run as a line rather than as a column.
When to take something else
| Need | Take |
|---|---|
| Uniform rows of “heading and subheading” | GrList |
| A single metric, large | GrStatistic |
| A set of objects in columns, with sorting | GrDataTable |
| A form section with a heading | GrFormSection |
| The values are editable | GrFormField |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrDescriptionList
:items="items"
labelWidth="7rem"
divided
/>Examples 5 each with a live demo and its code
Facts of one object
Rows describe one order, so the labels form a column of
label-widthand the values line up.namegives a pair its own#value-…slot — a badge, a link, an amount — andtonecolours the value, never the label.Order A-20419
3 items · ships from Rotterdam
- Status
- Unpaid
- Customer
- Margaret Hamilton
- Placed
- 14 Sep, 09:42
- Payment
- Card declined
- Total
- €1,284.50
Metadata as a line
layout="flow"lets short pairs run as a line under a title and wrap where they must — still a real<dl>withdtanddd, not text with colons.size="sm"takes a step down, and a tone marks the pair that needs attention.- Retry webhook deliveries with exponential backoff #4182
- Opened
- 2 h ago
- Reviews
- 2 of 2
- Checks
- Passing
- Changes
- +142 −38
- Move invoice PDFs to the EU bucket #4179
- Opened
- yesterday
- Reviews
- 0 of 1
- Checks
- 2 failing
- Changes
- +18 −4
- Drop the legacy CSV export endpoint #4171
- Opened
- 3 days ago
- Reviews
- Changes requested
- Checks
- Passing
- Changes
- +6 −412
- Retry webhook deliveries with exponential backoff #4182
Missing is not absent
nulland''keep their row, and the#emptyslot gets theitemto answer each one differently;0is a value. Long identifiers wrap instead of pushing the layout,#label-…adds a hint to a label, anddensity="compact"tightens the rows.- Endpoint ID
we_01J8ZQ4K7M3N9P2R5T6V8X0Y1Z- URL
- https://api.acme.test/hooks/billing/v2/events
- Signing secret
- whsec_••••••••••••3f9a
- Events
- invoice.paid, invoice.payment_failed, customer.subscription.updated
- Failed attempts
- 0
- Last delivery
- No deliveries yet
- Fallback URL
The container decides, not the screen
All four panels get the same props:
:columns="2",label-widthand:stack-below="240".columnsis a ceiling, so the wide panel fits two and a small card one — and in a card narrower thanstack-belowthe label goes above the value instead of squeezing it to a letter per line.db-prod-eu-2HealthyConfiguration
- Engine
- PostgreSQL 17.2
- Class
- 8 vCPU · 32 GB
- Storage
- 1.2 TB of 2 TB
- Region
- eu-central-1
- Replicas
- 2
- Backups
- Daily, kept 14 days
Connection
- Host
- db-prod-eu-2.internal.acme.test
- Port
- 5432
- TLS
- Required
Access
- Owner
- Platform team
- IP allowlist
- 3 ranges
- IAM auth
- Disabled
Maintenance
- Window
- Sun 02:00–03:00 UTC
- Next
- 12 Oct 2026
- Last patch
- 17.1 → 17.2
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The type size of the labels and the values by the controls scale. `md` by default. |
divided | boolean | undefined | undefined | Separate the pairs with rules. It does not apply in `flow`. |
emptyText | string | undefined | undefined | What to print an empty value with. |
columns | GrDescriptionColumns | undefined | undefined | The columns of the grid. They do not apply in `flow`: a line has no columns. |
layout | GrDescriptionLayout | undefined | undefined | `inline` — the label in a column on the left; `stacked` — above the value; `flow` — the pairs run along the line and wrap (metadata inside a list item). |
labelWidth | string | undefined | undefined | The width of the column of labels with `inline`. |
stackBelow | number | undefined | undefined | Below which width of the container (in px) `inline` switches to `stacked`. In a narrow column a fixed label squeezes the value down to one letter per line. |
density | GrDescriptionDensity | undefined | undefined | The step between the pairs: `compact` tightens it for dense cards. `regular` by default. |
itemsrequired | readonly GrDescriptionItem[] | — | The pairs in the order of output. The `name` of a pair opens its `#label-<name>` and `#value-<name>` slots. |
Slots
| Slot | Type | Description |
|---|---|---|
empty | { item: GrDescriptionItem; } | The empty state of a value — when an item has nothing to show. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrDescriptionLayout, GrDescriptionDensity, GrDescriptionColumns } from '@feugene/granularity/components/GrDescriptionList'Filedist/types/components/GrDescriptionList/config.d.ts
const grDescriptionListConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDescriptionList">;Filedist/types/components/GrDescriptionList/grDescriptionListStyles.d.ts
type GrDescriptionLayout = 'inline' | 'stacked' | 'flow';type GrDescriptionDensity = 'regular' | 'compact';type GrDescriptionColumns = 1 | 2 | 3 | 4;Filedist/types/components/GrDescriptionList/GrDescriptionList.vue.d.ts
interface GrDescriptionItem {
label: string;
value?: string | number | null;
name?: string;
tone?: GrTone;
}interface GrDescriptionListProps {
items: readonly GrDescriptionItem[];
layout?: GrDescriptionLayout;
labelWidth?: string;
stackBelow?: number;
density?: GrDescriptionDensity;
divided?: boolean;
columns?: GrDescriptionColumns;
emptyText?: string;
size?: GrComponentSize;
}Filedist/types/components/GrDescriptionList/defaults.d.ts
interface GrDescriptionListConfigurableProps {
size: GrComponentSize;
layout: GrDescriptionLayout;
labelWidth: string;
density: GrDescriptionDensity;
divided: boolean;
columns: GrDescriptionColumns;
emptyText: string;
}Filedist/types/components/GrDescriptionList/safelist.d.ts
const grDescriptionListSafelist: string[];Usage from the library prose
dl > div > dt + dd, not a <div> that merely looks alike
A pair is wrapped into a <div>: such nesting is valid by HTML5 and is needed for the
layout. That is the very reason the component exists — hand-written markup twice
produced a <dl> with bare <div> elements inside. It looked like a list, but it was
not a pair of terms: neither for a parser nor for a screen reader was there a property
or a value there.
An empty value is printed rather than skipped
null, undefined and '' give a dash (emptyText, the #empty slot), but the row
stays. “The field exists, the value does not” and “the field does not exist” are
different statements; a dropped row breaks both the alignment and the reading.
Zero does not count as emptiness: 0 is a value.
The layout gives in to the width of the container
inline keeps the labels in a column of labelWidth, otherwise the values shift on
every row by the length of the label — exactly what the column is there for.
stackBelow (in pixels) switches the layout to stacked when the container is
narrower than the threshold: in a narrow column a fixed label squeezes the value down
to one letter per line. It is the container that is measured, not the viewport —
pairs live in a narrow card on a wide screen too. Before mounting, the layout that was
set applies, so the server render is stable.
The threshold applies only to inline: the column of labels is the only thing that
breaks in a narrow container. flow wraps by itself, and there is nothing to replace
it with.
columns lays the pairs out with a grid rather than with column-count: a pair is a
grid item and is not torn between columns.
columns is a ceiling, not an order
The number of columns is chosen by CSS from the width of the container:
columns: 4 means “up to four”. If there is room for four, there will be four; if
there is room for two, there will be two; and in a column of 290px there will be one.
Media queries are unsuitable here in principle: they measure the screen, while the list lives in a card. On a wide monitor two columns would switch on inside a narrow one, a label of fixed width would eat almost all of the room, and the value would wrap character by character — “30” printed as “3” and “0” on two lines. The number shown becomes wrong, and that is no longer a matter of taste.
The threshold below which a column is not divided is
--gr-description-list-column-min (12rem by default). Everything is computed in pure
CSS: no measurements, no JS, no divergence between the server and the client.
With inline the threshold is higher: the label and the value stand side by side,
so the column has to hold both — labelWidth plus
--gr-description-list-value-min (5rem). Without that addition a column as wide as a
single label leaves the value a few pixels, and it is the value that then wraps
character by character: the same defect, only in other numbers. In stacked and flow
the width of the label does not enter the calculation — the value is under it there,
not beside it.
flow — metadata as a line rather than as a column
inline and stacked both put the pairs one under another. When the pairs are short
and serve as a caption to something else — “Messages: 3 · Created: 12.04 · Last:
yesterday” inside a list item — a column is not what is needed: what is needed is a
line that flows across the width and wraps.
<GrDescriptionList :items="meta" layout="flow" />The root is not a grid here, so columns, divided and labelWidth do not apply: a
line has neither columns nor a row that can be marked off with a rule.
The semantics does not suffer in the process — it is still a real <dl> with dt/dd
pairs, not text with colons.
The tone belongs to the value only
A red label reads as “the field is broken”, although the problem is with the quantity.
That is why the tone of a pair colours the <dd> and does not touch the <dt>.
The slots are named after name
#value-<name> and #label-<name> work for pairs that have a name set: a GrBadge,
a relative time and links regularly stand in the pairs. Pairs without a name remain
strings — the key exists precisely so that a slot addresses one pair rather than all of
them at once.
Limits
The component does not edit the values, does not sort the pairs and does not group them
into sections. Sections are a GrCard with a title around several lists.
Props in this example