GrDescriptionList
Берут, когда карточка объекта.
Когда брать
- карточка объекта — сводка заказа, профиля, документа: подписи слева, значения колонкой справа;
- технические реквизиты — идентификаторы, хеши, MIME, версии: длинное значение переносится, а не выталкивает вёрстку;
- часть значений — не текст — бейдж статуса, относительное время, ссылка подставляются слотом на конкретную пару;
- пар много — до четырёх колонок складывают длинный список, не разрывая пары;
- метаданные подписью к чему-то другому — «Сообщений: 3 · Создана: 12.04»
внутри пункта списка:
flowпускает пары строкой, а не столбцом.
Когда взять другое
| Нужно | Берите |
|---|---|
| Однородные строки «заголовок и подзаголовок» | GrList |
| Один показатель крупно | GrStatistic |
| Набор объектов по колонкам, с сортировкой | GrDataTable |
| Секция формы с заголовком | GrFormSection |
| Значения редактируются | GrFormField |
`dl > div > dt + dd`, а не `<div>` с виду похожий
Пара оборачивается в <div>: такая вложенность валидна по HTML5 и нужна для
раскладки. Это и есть причина существования компонента — ручная разметка
дважды дала <dl> с голыми <div> внутри. Выглядело списком, но парой
терминов не было: ни для парсера, ни для скринридера там не было ни
характеристики, ни значения.
Пустое значение печатается, а не пропускается
null, undefined и '' дают прочерк (emptyText, слот #empty), но строка
остаётся. «Поле есть, значения нет» и «поля нет» — разные утверждения;
выпавшая строка ломает и выравнивание, и чтение.
Ноль пустотой не считается: 0 — это значение.
Раскладка сдаётся по ширине контейнера
inline держит подписи колонкой шириной labelWidth, иначе значения
сдвигаются на каждой строке по длине подписи — ровно то, ради чего колонка и
нужна.
stackBelow (в пикселях) переключает раскладку на stacked, когда контейнер
уже порога: в узкой колонке фиксированная подпись выжимает значение в букву на
строку. Меряется контейнер, а не вьюпорт — пары живут и в узкой карточке на
широком экране. До монтирования действует заданная раскладка, поэтому серверный
рендер стабилен.
Порог применяется только к inline: колонка подписей — единственное, что в
узком контейнере ломается. flow переносится сам, и подменять его нечем.
columns раскладывает пары сеткой, а не column-count: пара — элемент сетки и
между колонками не рвётся.
`columns` — это потолок, а не приказ
Число колонок выбирает CSS по ширине контейнера: columns: 4 значит «до
четырёх». Хватает места на четыре — будет четыре, хватает на две — будет две, а
в колонке 290px останется одна.
Медиазапросы тут не годятся принципиально: они меряют экран, а список живёт в карточке. На широком мониторе в узкой колонке включались бы две колонки, подпись фиксированной ширины съедала бы почти всё место, и значение переносилось бы посимвольно — «30» печаталось как «3» и «0» на двух строках. Показанное число становится неверным, и это уже не вопрос вкуса.
Порог, ниже которого колонка не делится, — --gr-description-list-column-min
(по умолчанию 12rem). Считается всё чистым CSS: ни замеров, ни JS, ни
расхождения между сервером и клиентом.
При inline порог выше: подпись и значение стоят рядом, поэтому колонка
обязана вместить обоих — labelWidth плюс --gr-description-list-value-min
(5rem). Без этой добавки колонка шириной с одну подпись оставляет значению
несколько пикселей, и уже оно переносится посимвольно: тот же дефект, только в
других числах. В stacked и flow ширина подписи в расчёт не идёт — значение
там под ней, а не рядом.
`flow` — метаданные строкой, а не столбцом
inline и stacked оба ставят пары друг под другом. Когда пары короткие и
служат подписью к чему-то другому — «Сообщений: 3 · Создана: 12.04 · Последнее:
вчера» внутри пункта списка, — колонка не нужна: нужна строка, которая течёт по
ширине и переносится.
<GrDescriptionList :items="meta" layout="flow" />
Корень здесь не сетка, поэтому columns, divided и labelWidth не
применяются: в строке нет ни колонок, ни ряда, который можно отбить линией.
Семантика при этом не страдает — это по-прежнему настоящий <dl> с парами
dt/dd, а не текст с двоеточиями.
Тон — только у значения
Красная подпись читается как «поле сломано», хотя проблема в величине. Поэтому
tone у пары красит <dd> и не трогает <dt>.
Слоты именуются по `name`
#value-<name> и #label-<name> работают для пар, у которых задан name:
в парах регулярно стоят GrBadge, относительное время и ссылки. Пары без
name остаются строками — ключ нужен именно затем, чтобы слот адресовался к
одной паре, а не ко всем сразу.
Границы
Компонент не редактирует значения, не сортирует пары и не группирует их в
секции. Секции — GrCard с title вокруг нескольких списков.
Playground 5
Загружается…
<GrDescriptionList />Установка
npm i @feugene/granularityИмпорт
import { GrDescriptionList } from '@feugene/granularity/components/GrDescriptionList'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
divided | boolean | undefined | undefined | Отбивать пары линиями. В `flow` не применяется. |
emptyText | string | undefined | undefined | Чем печатать пустое значение. |
columns | GrDescriptionColumns | undefined | undefined | Колонки сетки. В `flow` не применяются: строка колонок не имеет. |
layout | GrDescriptionLayout | undefined | undefined | `inline` — подпись слева колонкой; `stacked` — над значением; `flow` — пары текут по строке с переносом (метаданные внутри пункта списка). |
labelWidth | string | undefined | undefined | Ширина колонки подписей при `inline`. |
stackBelow | number | undefined | undefined | Ниже какой ширины контейнера (px) `inline` переключается на `stacked`. В узкой колонке фиксированная подпись выжимает значение в букву на строку. |
density | GrDescriptionDensity | undefined | undefined | — |
itemsобязательный | readonly GrDescriptionItem[] | — | — |
Slots
| Slot | Type | Описание |
|---|---|---|
empty | { item: GrDescriptionItem; } | Пустое состояние значения — когда у пункта нечего показать. |
Примеры 2
Сводка объекта настоящим `<dl>`
Пары «характеристика и значение» с выравниванием значений в колонку. Слот адресуется по name пары, поэтому статус может быть бейджем, а не строкой; пустое значение печатается прочерком и строку не теряет.
- Status
- Active
- Created
- 17 August 2026
- Owner
- billing-team
- X-Request-Id
- req_01HXQZ8K3M7N2P4R6T8V0W1Y3Z
- Archived at
- —
<script setup lang="ts">
import type { GrDescriptionItem } from '@feugene/granularity'
import { GrBadge, GrDescriptionList } from '@feugene/granularity'
const items: GrDescriptionItem[] = [
{ label: 'Status', value: 'Active', name: 'status' },
{ label: 'Created', value: '17 August 2026' },
{ label: 'Owner', value: 'billing-team' },
{ label: 'X-Request-Id', value: 'req_01HXQZ8K3M7N2P4R6T8V0W1Y3Z' },
{ label: 'Archived at', value: null },
]
</script>
<template>
<GrDescriptionList :items="items" label-width="9rem" divided>
<!-- Слот адресуется по `name` пары: в парах регулярно стоят бейджи,
относительное время и ссылки, а не только строки. -->
<template #value-status>
<GrBadge tone="success">
Active
</GrBadge>
</template>
</GrDescriptionList>
</template>Разметка — dl > div > dt + dd: она валидна по HTML5 и нужна для раскладки. Ручная вёрстка регулярно даёт <dl> с голыми <div> внутри — выглядит списком, но парой терминов не является ни для парсера, ни для скринридера.
Колонкой, стопкой, строкой и в несколько колонок
inline держит подписи колонкой, stacked ставит их над значением, flow пускает пары строкой с переносом. columns задаёт потолок колонок — сколько встанет на самом деле, решает ширина контейнера, а не вьюпорта: потяните ползунок и посмотрите, как две колонки схлопываются в одну на широком экране. stackBelow тем же способом переключает раскладку подписей.
- Plan
- Business
- Seats
- 48
- MIME
- application/pdf
- Size
- 2.4 MB
- Checksum
- sha256:9f2b1c…
- Retention
- 90 days
<script setup lang="ts">
import { ref } from 'vue'
import type { GrDescriptionItem, GrDescriptionColumns, GrDescriptionLayout } from '@feugene/granularity'
import { GrDescriptionList, GrSegmented } from '@feugene/granularity'
const items: GrDescriptionItem[] = [
{ label: 'Plan', value: 'Business' },
{ label: 'Seats', value: 48 },
{ label: 'MIME', value: 'application/pdf' },
{ label: 'Size', value: '2.4 MB' },
{ label: 'Checksum', value: 'sha256:9f2b1c…' },
{ label: 'Retention', value: '90 days' },
]
const layout = ref<GrDescriptionLayout>('inline')
const columns = ref<GrDescriptionColumns>(2)
/**
* Ширина контейнера, а не окна: колонки считает CSS от неё. Сузьте — и лишние
* колонки схлопнутся сами, не дожидаясь смены брейкпоинта вьюпорта.
*/
const width = ref(680)
</script>
<template>
<div class="grid gap-4">
<GrSegmented
v-model="layout"
:options="[
{ value: 'inline', label: 'inline' },
{ value: 'stacked', label: 'stacked' },
{ value: 'flow', label: 'flow' },
]"
size="sm"
/>
<!--
Колонки принадлежат сетке, поэтому в `flow` переключатель не у дел:
строка раскладывает пары по ширине и переносит их сама.
-->
<GrSegmented
v-model="columns"
:options="[
{ value: 1, label: '1' },
{ value: 2, label: '2' },
{ value: 3, label: '3' },
{ value: 4, label: '4' },
]"
:disabled="layout === 'flow'"
size="sm"
/>
<!--
Ширина контейнера, а не окна. Колонки считает CSS от неё: `columns` задаёт
потолок, а сколько их встанет на самом деле — решает место. Сузьте до
290px, и две колонки схлопнутся в одну, хотя экран остался широким.
-->
<label class="flex items-center gap-3 text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
Ширина контейнера
<input v-model.number="width" type="range" min="240" max="680" step="10" class="w-48">
<span class="tabular-nums">{{ width }}px</span>
</label>
<!--
`stackBelow` меряет контейнер и переключает только раскладку подписей:
фиксированная подпись в узкой колонке выжимает значение в букву на строку.
-->
<div :style="{ width: `${width}px`, maxWidth: '100%' }" class="rounded-[var(--gr-radius-md)] border border-dashed border-[var(--gr-brd)] p-3">
<GrDescriptionList :items="items" :layout="layout" :columns="columns" :stack-below="420" />
</div>
</div>
</template>