GrDescriptionList

  • core
  • data
  • API · 9 props

Lists label-value pairs as a real description list, values aligned in a column.

import { GrDescriptionList } from '@feugene/granularity/components/GrDescriptionList'

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: flow lets the pairs run as a line rather than as a column.

When to take something else

NeedTake
Uniform rows of “heading and subheading”GrList
A single metric, largeGrStatistic
A set of objects in columns, with sortingGrDataTable
A form section with a headingGrFormSection
The values are editableGrFormField

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

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-width and the values line up. name gives a pair its own #value-… slot — a badge, a link, an amount — and tone colours the value, never the label.

    Order A-20419

    3 items · ships from Rotterdam

    Status
    Unpaid
    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> with dt and dd, not text with colons. size="sm" takes a step down, and a tone marks the pair that needs attention.

  • Missing is not absent

    null and '' keep their row, and the #empty slot gets the item to answer each one differently; 0 is a value. Long identifiers wrap instead of pushing the layout, #label-… adds a hint to a label, and density="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-width and :stack-below="240". columns is a ceiling, so the wide panel fits two and a small card one — and in a card narrower than stack-below the label goes above the value instead of squeezing it to a letter per line.

    db-prod-eu-2Healthy

    Configuration

    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

PropTypedefaultDescription
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe type size of the labels and the values by the controls scale. `md` by default.
dividedboolean | undefinedundefinedSeparate the pairs with rules. It does not apply in `flow`.
emptyTextstring | undefinedundefinedWhat to print an empty value with.
columnsGrDescriptionColumns | undefinedundefinedThe columns of the grid. They do not apply in `flow`: a line has no columns.
layoutGrDescriptionLayout | undefinedundefined`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).
labelWidthstring | undefinedundefinedThe width of the column of labels with `inline`.
stackBelownumber | undefinedundefinedBelow 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.
densityGrDescriptionDensity | undefinedundefinedThe step between the pairs: `compact` tightens it for dense cards. `regular` by default.
itemsrequiredreadonly GrDescriptionItem[]—The pairs in the order of output. The `name` of a pair opens its `#label-<name>` and `#value-<name>` slots.

Slots

SlotTypeDescription
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
import type { GrDescriptionLayout, GrDescriptionDensity, GrDescriptionColumns } from '@feugene/granularity/components/GrDescriptionList'

Filedist/types/components/GrDescriptionList/config.d.ts

grDescriptionListConfigconst
const grDescriptionListConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDescriptionList">;

Filedist/types/components/GrDescriptionList/grDescriptionListStyles.d.ts

GrDescriptionLayouttype
type GrDescriptionLayout = 'inline' | 'stacked' | 'flow';
GrDescriptionDensitytype
type GrDescriptionDensity = 'regular' | 'compact';
GrDescriptionColumnstype
type GrDescriptionColumns = 1 | 2 | 3 | 4;

Filedist/types/components/GrDescriptionList/GrDescriptionList.vue.d.ts

GrDescriptionIteminterface
interface GrDescriptionItem {
    label: string;
    value?: string | number | null;
    name?: string;
    tone?: GrTone;
}
GrDescriptionListPropsinterface
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

GrDescriptionListConfigurablePropsinterface
interface GrDescriptionListConfigurableProps {
    size: GrComponentSize;
    layout: GrDescriptionLayout;
    labelWidth: string;
    density: GrDescriptionDensity;
    divided: boolean;
    columns: GrDescriptionColumns;
    emptyText: string;
}

Filedist/types/components/GrDescriptionList/safelist.d.ts

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

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