Descriptions

Free

Key–value list for the detail panel of a record — real <dl>/<dt>/<dd> semantics, 1–4 responsive columns with per-item span, horizontal or vertical layouts, and a value that is missing told apart from one that is blank.

</>sv-descriptions detailkey-valuerecord

Complexity tier: Primitive Presents a value: held to semantic markup and contrast, with its right-to-left and print gaps recorded and the right-to-left debt allowed only to shrink.

import { SvDescriptionsComponent } from '@styloviz/display';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Vertical, with a full-width note

example.htmlAngular
<sv-descriptions
  layout="vertical"
  [columns]="3"
  [items]="[
    { id: 'sku',   label: 'SKU',   value: 'ORD-4182' },
    { id: 'total', label: 'Total', value: '$248.00' },
    { id: 'notes', label: 'Notes', value: 'Leave with the concierge.', span: 3 },
  ]" />

Wired to an API

example.htmlAngular
<sv-descriptions
  [items]="order()?.fields ?? []"
  [loading]="pending()"
  [error]="failure()"
  (retry)="reload()" />

An action per row, without breaking the reading model

A <dd> is the DEFINITION of its term, and HTML leaves nowhere else to put a control: inside a <dl>'s wrapping <div> only <dt> and <dd> are allowed, so there is no sibling position. The action therefore lives inside the definition, which makes two things compulsory rather than stylistic. It is laid out as its own column, so the value text keeps its alignment instead of being shoved around by a button. And its accessible name must carry the term — "Copy order total", not "Copy" — because a listener who lands on it is inside the definition and has no other way to know which pair they are in. The template gets the item so you can say it; the component cannot enforce it for you.

example.htmlAngular
<sv-descriptions [items]="details">
  <ng-template svDescriptionAction let-item>
    <button type="button"
            [attr.aria-label]="'Copy ' + item.label"
            (click)="copy(item)">Copy</button>
  </ng-template>
</sv-descriptions>

Stacking on a phone

A horizontal pair sits side by side above the breakpoint and stacks below it. The rule was always in the class list and never in the API — the layout has been sm:flex since the beginning, so a host had no say and no way to find out. stackAt states it and moves it; 'never' keeps the pair side by side at every width, which only works when the labels are genuinely short. The part that was actually broken: a stacked pair had no vertical spacing at all, so on a phone the label sat directly on its value and read as one wrapped sentence.

example.htmlAngular
<sv-descriptions [items]="details" stackAt="md" />
<sv-descriptions [items]="shortLabels" stackAt="never" />

When to use

  • The read-only half of a record page — view order, user profile, resource detail, invoice header.
  • Any place a grid of hand-rolled <span> pairs is standing in for a description list.
  • For editable fields use a Form; for many records at once use a Data Table.

Async states

  • loading
  • empty
  • error
  • retry

Inputs

NameTypeDefaultDescription
itemsreadonly DescriptionItem[][]The term/value pairs to render, in order.
titlestring''Heading above the list. Also becomes the list's accessible name.
layoutDescriptionsLayout'horizontal'Where the label sits relative to its value.
variantDescriptionsVariant'plain'Surface treatment.
sizeDescriptionsSize'md'Padding and type scale.
columnsDescriptionsColumns1Maximum pairs per row at the widest breakpoint.
stackAtDescriptionsStackAt'sm'Where a `horizontal` pair stops sitting side by side and stacks.
emptyValueTextstring'—'Shown in place of a value that is `null` or `undefined`.
customClassstring''Extra classes on the root element.
loadingbooleanfalseReplace the list with a skeleton while the record is being fetched.
errorSvDataError | string | nullnullLoad failure. Use `toSvDataError()` from `@styloviz/core` to normalise an `HttpErrorResponse`.
emptyTextstring'No details to show'Message for the empty state.

Outputs

EventPayload typeDescription
retryvoidThe host should re-run the failed load.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Bound to the names this component actually exposes.

usage.htmlAngular
<sv-descriptions
  [items]="data.items()"
  [loading]="data.loading()"
  [error]="data.error()"
  (retry)="data.reload()"
/>

Host class

Drive the three async inputs from your own fetch.

host.component.tsTypeScript
// This component reports its state but does not emit a query, so drive
// it from your own fetch and bind the three async inputs.
readonly items = signal<Row[]>([]);
readonly loading = signal(false);
readonly error = signal<SvDataError | null>(null);

async load(): Promise<void> {
  this.loading.set(true);
  this.error.set(null);
  try {
    this.items.set(await this.api.fetchRows());
  } catch (e) {
    this.error.set(toSvDataError(e));
  } finally {
    this.loading.set(false);
  }
}
  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `items`, `loading` and `error`.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • `svDiffValues()` from `@styloviz/core` turns two records into a list of changed paths, which maps straight onto `DescriptionItem[]` for a before/after view — the same engine Code Block and JSON Viewer use, rather than a fourth one.
  • Lay out with `layout` (horizontal / vertical), `columns` (1–4, stepping down on narrow viewports) and a per-item `span` that is clamped to the row width.
  • Choose a surface with `variant` (plain / bordered) and a scale with `size` (sm / md / lg).
  • Set `emptyValueText` for the missing-value placeholder, and `valueClass` per item (e.g. `font-mono` for an ID).
  • Full async contract: `loading`, `error`, `emptyText` and a `retry` output.
  • `<ng-template svDescriptionAction let-item>` adds one action per row, rendered as its own column inside the definition and given the item so the action can name the term it acts on.
  • `stackAt` (`never` / `sm` / `md` / `lg`) sets where a horizontal pair stops sitting side by side and stacks.

Best practices

  • Do: Send `null` for a field you do not have and `''` for one that is genuinely blank — they render differently on purpose.
  • Do: Give a long value its own row with `span` rather than letting it squeeze the column.
  • Tip: Pass `title` so the list gets an accessible name; it is wired to `aria-labelledby` for you.
  • Don't: Don't use it for a collection of records — it describes one.
  • Do: Name a row action with the term it acts on — `'Copy ' + item.label`. Inside a definition, "Copy" on its own tells a listener nothing about what is being copied.
  • Don't: Don't put a second list, a form control or a group of buttons inside a value. A `<dl>` is a flat sequence of pairs, and nesting turns the reading into a depth-first walk nobody can predict. One action per row is what the slot is sized for.
  • Tip: Reach for `stackAt="never"` only with short labels. Two columns on a 320px screen leave the value about eleven characters, and a truncated value is worse than a stacked one.

Accessibility

  • Renders a real `<dl>` with one `<dt>`/`<dd>` pair per item, each wrapped in a `<div>` — valid HTML5 that keeps the pairing screen readers announce.
  • A `title` becomes an `<h3>` and names the list via `aria-labelledby`; ids are instance-scoped, so two lists on one page never collide.
  • No `aria-labelledby` is emitted without a heading, so the fallback name is never suppressed.
  • A missing value shows a dimmed placeholder rather than an empty cell that reads as a rendering bug.
  • A row action is rendered inside its `<dd>` because HTML permits no sibling there, so it becomes part of what the definition reads as — which is why the docs insist its accessible name carries the term. It is laid out as a separate column so the value text keeps its alignment.
  • A stacked pair gets real vertical spacing, so a label and its value do not read as one wrapped sentence on a narrow screen.

Feedback on Descriptions

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.