Descriptions
A read-only list of label and value pairs, laid out in columns, for the fields of one record
Installation
pnpm add @talex-touch/tuffex
import { TxDescriptions, TxDescriptionsItem } from '@talex-touch/tuffex/descriptions'
// One sheet for both components
import '@talex-touch/tuffex/descriptions/style.css'
import '@talex-touch/tuffex/base.css' // tokens + resets, once per app
Usage
TxDescriptions shows the fields of one record as label and value pairs: a member in an admin drawer, a run's details, a plugin's metadata. Each TxDescriptionsItem is one field, and the value goes in its default slot.
Descriptions
Two columns by default. phone is empty, so its value shows the — placeholder; credits is 0, which is a value and shows as one.
Layout and size
horizontal puts each label in a track beside its value, and the labels of one column share that track, so the values line up. vertical puts the label above its value, which suits narrow columns and long values. sm is for drawers and side panels, where the surrounding text is 13px.
Columns and span
span gives a long field more than one column and is clamped to columns. Below 480px of container the list falls back to one column whatever columns says; switch the frame to 360px to see it.
Best Practices
- One item per field, in reading order: the list fills each row left to right.
- Leave a missing value empty instead of writing
—yourself, so every list shows missing data the same way. PassemptyTextonce for a different placeholder, such as a localized "Not set". - Give a long field (an ID, an address, a note) a
spanrather than loweringcolumnsfor the whole list. - Keep labels short. A label wider than 40% of its column wraps; set
labelWidthwhen several lists stacked on one page must line up with one another. - Use
size="sm"inside drawers and side panels, andmdon a page. - Let the parent give the list its width. In a flex row, give it
flex: 1; in a shrink-to-fit parent the container has no width of its own and collapses. - Put the items directly in the slot.
v-forandv-ifare fine, but a wrapping element breaks both the<dl>and the grid.
API Reference
TxDescriptions Props
| Name | Type | Default | Description |
|---|---|---|---|
columns | number | 2 | Items per row, floored; anything below 1 is 1. Below 480px of container the list falls back to one column whatever this says |
layout | 'horizontal' | 'vertical' | 'horizontal' | horizontal: the label in its own track beside the value, the labels of a column sharing one track. vertical: the label above its value. Any other value is horizontal |
size | 'sm' | 'md' | 'md' | md: 14px values and 13px labels, 12px between rows, 24px between pairs. sm: 13px values and labels, 8px / 16px |
emptyText | string | '—' | Shown in place of a value that renders nothing: no default slot, only whitespace, an empty interpolation or a false v-if. 0 is a value |
labelWidth | string | number | - | Width of the label track in the horizontal layout; a number is px. Left unset, each column's labels share the width of the longest one, up to 40% of the column. The vertical layout ignores it |
TxDescriptionsItem Props
| Name | Type | Default | Description |
|---|---|---|---|
label | string | '' | Label text. The label slot replaces it |
span | number | 1 | Columns to span, floored and clamped between 1 and the list's columns. Below 480px of container every item takes the whole row |
Slots
| Component | Slot | Description |
|---|---|---|
TxDescriptions | default | The TxDescriptionsItems, directly: a wrapping element breaks the <dl> and the grid |
TxDescriptionsItem | default | The value. When it renders nothing, emptyText shows instead |
TxDescriptionsItem | label | Replaces the label text, inside the same <dt> |
CSS Variables
| Variable | Default | Description |
|---|---|---|
--tx-descriptions-gap | 12px; sm 8px | Between rows, and between a label and its value in the horizontal layout. Pairs sit twice this apart; in the vertical layout the label sits a quarter of it above its value |
--tx-descriptions-font-size | 14px; sm 13px | Value size; labels are 1px smaller, but never under 13px |
--tx-descriptions-label-width | unset | Width of the label track. labelWidth writes it on the root; a host rule on the root or an ancestor works too |
smsets the first two on the root at one class's weight, so a host rule with more weight than one class (a scoped class counts) overrides them in any load order.- The component writes
--tx-descriptions-columnson the root and--tx-descriptions-spanon each item; they are not meant for hosts.
Types
type DescriptionsLayout = 'horizontal' | 'vertical'
type DescriptionsSize = 'sm' | 'md'
Overview
- The root,
div.tx-descriptionswithtx-descriptions--{layout}andtx-descriptions--{size}, is a query container arounddl.tx-descriptions__list. Each item is adiv.tx-descriptions__itemholding onedt.tx-descriptions__labeland onedd.tx-descriptions__value, the grouping HTML allows inside a<dl>, so assistive technology reads each pair as a term and its description. - Horizontal layout: the list is a grid of
columnspairs, each a label track and a value track. Every item is a subgrid over its own pair, so all the labels of a column share one track. The track is as wide as the longest label, up to 40% of the column, after which labels wrap; every value of that column starts at the same place, on the first baseline of its label. - A label sits
--tx-descriptions-gapfrom its value and pairs sit twice that apart, so the pairs read as groups. spancovers whole pairs. Items fill the rows in source order, and an item that does not fit what is left of a row starts the next one, leaving the gap empty.- Below 480px of its own width (a container query, not the viewport) the list is one column and every item takes the whole row. The width has to come from the parent: a shrink-to-fit parent (an inline-block, a flex item that does not stretch, an
autogrid track) gives an inline-size container no width of its own. - A value counts as empty when its slot renders nothing but comments and whitespace: no slot,
{{ '' }},{{ null }}, a falsev-if, an emptyv-for. ThenemptyTextrenders indd.is-empty.0and any element, even an empty<span>, count as content. The check runs on every render, so a value that arrives later replaces the placeholder. - Values wrap anywhere (
overflow-wrap: anywhere): a long ID, email or URL wraps inside its column instead of widening it. - Values use
--tx-text-color-primary. Labels and the empty placeholder use--tx-text-color-regular, because 13px text has to clear 4.5:1 and--tx-text-color-secondarydoes not. - There is no motion and nothing to operate. Server rendering emits the same markup and inline custom properties; nothing reads the DOM.
Technologies
- Source:
packages/tuffex/packages/components/src/descriptions/src/TxDescriptions.vue(the list, the layout and every rule, items included) andTxDescriptionsItem.vue(the pair, its span and the empty check); types intypes.ts. The list hands its itemslayout,columnsandemptyTextthroughprovide/inject. - CSS grid
subgridlines the labels up;@container (width < 480px)on the root drives the one-column fallback. The list is a separate element inside the root because a container query cannot restyle the container itself. - The stylesheet is not scoped: every selector carries the
tx-descriptionsprefix, and both size tiers come from two custom properties. That keeps the sheet small against the CSS size gate. - The empty check walks the slot's vnodes: comments, whitespace-only text and fragments of those are empty. Vue's own slot fallback skips comments only, so
{{ user.phone }}without a phone would otherwise render a blank value. - Rejected: a two-column grid inside each item with a fixed default label width. Values line up only while every label fits that width, and a spanning item's value starts somewhere else. A
<table>was rejected too: it claims row and column relations the content does not have. - Verified coverage:
descriptions/__tests__/descriptions.test.ts(10 cases) covers the<dl>/dt/ddstructure and thelabelslot; the column count and its fallback for 0, negative,NaNand fractional input; spans doubled into label and value tracks, clamped, and kept as columns in the vertical layout; the empty check for no slot, whitespace, an empty interpolation, a falsev-ifand an emptyv-for, with0and elements kept; a custom and an updatedemptyText; a value switching between empty and filled; layout and size classes with unknown values falling back;labelWidthin px, as a string, and dropped in the vertical layout; an item outside a list; and, against the stylesheet, the narrow-container rule, the subgrid and the prefix on every unscoped selector.
Use cases
- The fields of one record in a drawer or a detail panel: a member, a subscription, a run, a plugin.
- A summary above a form or a table, naming what the next screen acts on.
- Read-only settings and metadata inside a card.