GrDescriptionList

Пакет: @feugene/granularityядроГруппа: Данные

Берут, когда карточка объекта.

Когда брать

  • карточка объекта — сводка заказа, профиля, документа: подписи слева, значения колонкой справа;
  • технические реквизиты — идентификаторы, хеши, 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

PropTypeпо умолчаниюОписание
size"xs" | "sm" | "md" | "lg" | undefinedundefined
dividedboolean | undefinedundefinedОтбивать пары линиями. В `flow` не применяется.
emptyTextstring | undefinedundefinedЧем печатать пустое значение.
columnsGrDescriptionColumns | undefinedundefinedКолонки сетки. В `flow` не применяются: строка колонок не имеет.
layoutGrDescriptionLayout | undefinedundefined`inline` — подпись слева колонкой; `stacked` — над значением; `flow` — пары текут по строке с переносом (метаданные внутри пункта списка).
labelWidthstring | undefinedundefinedШирина колонки подписей при `inline`.
stackBelownumber | undefinedundefinedНиже какой ширины контейнера (px) `inline` переключается на `stacked`. В узкой колонке фиксированная подпись выжимает значение в букву на строку.
densityGrDescriptionDensity | undefinedundefined
itemsобязательныйreadonly GrDescriptionItem[]

Slots

SlotTypeОписание
empty{ item: GrDescriptionItem; }Пустое состояние значения — когда у пункта нечего показать.

Примеры 2

Сводка объекта настоящим `<dl>`

Пары «характеристика и значение» с выравниванием значений в колонку. Слот адресуется по name пары, поэтому статус может быть бейджем, а не строкой; пустое значение печатается прочерком и строку не теряет.

Status
Active
Created
17 August 2026
Owner
billing-team
X-Request-Id
req_01HXQZ8K3M7N2P4R6T8V0W1Y3Z
Archived at

Basic
<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

Layout
<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>

Документация компонентаВсе компоненты