GrTable
Берут, когда ячейки оформляет потребитель.
Обзор
Когда брать
- ячейки оформляет потребитель — своя разметка
<tr>/<td>, а от компонента нужны только обвязка и поведение; - таблица шире экрана — скролл-обёртка достижима с клавиатуры и объявлена как регион;
- заголовок должен оставаться видимым —
stickyHeaderвместе сmaxHeight; - колонка-подпись должна оставаться видимой —
stickyColumnпри горизонтальной прокрутке; - таблица уже есть и её не переписывают — обёртка добавляет
caption, состояния и скролл, не трогая разметку.
Когда взять другое
| Нужно | Берите |
|---|---|
| Нужны сортировка, выбор строк, слоты ячеек | GrDataTable |
| Строки однородные и без столбцов | GrList |
| Строки вложены | GrTree |
| Порядок строк меняет пользователь | GrSortableList |
| Данные показывают графиком | GrChartBar |
caption — не украшение: без него таблица в списке ориентиров скринридера
безымянна, и на странице с тремя таблицами их не различить.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrTable caption="Invoices" emptyText="No invoices yet">
<tr><td class="whitespace-nowrap px-4 py-2">Northwind</td><td class="whitespace-nowrap px-4 py-2">Oct 15</td><td class="whitespace-nowrap px-4 py-2 text-right [font-variant-numeric:tabular-nums]">€1,284.50</td></tr>
<tr><td class="whitespace-nowrap px-4 py-2">Contoso</td><td class="whitespace-nowrap px-4 py-2">Oct 12</td><td class="whitespace-nowrap px-4 py-2 text-right [font-variant-numeric:tabular-nums]">€432.00</td></tr>
<tr><td class="whitespace-nowrap px-4 py-2">Fabrikam</td><td class="whitespace-nowrap px-4 py-2">Oct 2</td><td class="whitespace-nowrap px-4 py-2 text-right [font-variant-numeric:tabular-nums]">€96.00</td></tr>
</GrTable>Примеры 5 у каждого — живое демо и его код
Разметка ваша, рамка таблицы
Строки пишете вы, поэтому в ячейке может быть что угодно — две строки текста, бейдж, просроченная дата красным. Таблица даёт рамку, тонированную шапку, подсветку строки и
captionдля диктора;#footerставит итог в<tfoot>, ровно под его колонку.InvoicesOctober 2026 · 4 invoicesInvoices, October 2026 Customer Status Due Amount Northwind TradersINV-0421 · [email protected]Sent Oct 21 €1,284.50 Contoso LtdINV-0420 · [email protected]Overdue Oct 2 €432.00 Tailspin ToysINV-0419 · [email protected]Paid Sep 30 €2,150.00 Fabrikam IncINV-0418 · [email protected]Draft — €96.00 Outstanding €1,716.50 Длинная выписка в постоянной высоте
max-heightпрокручивает строки внутри таблицы, а не страницу,sticky-headerдержит названия колонок на виду, аstripedведёт взгляд по строке мелкого кегля.scrollToRowдоходит до строки по её индексу — здесь до места, где остановилась сверка.Business account21 transactions to reconcileTransactions, business account Date Payee Category Amount Oct 6 Stripe payout Sales +€120.00 Oct 5 Lufthansa Travel −€499.00 Oct 4 Northwind Traders Sales +€878.00 Oct 3 AWS Hosting −€1,257.00 Oct 2 Contoso Ltd Sales +€1,636.00 Oct 1 WeWork Office −€2,015.00 Sep 30 Figma Software −€2,394.00 Sep 29 Stripe payout Sales +€373.00 Sep 28 Lufthansa Travel −€752.00 Sep 27 Northwind Traders Sales +€1,131.00 Sep 26 AWS Hosting −€1,510.00 Sep 25 Contoso Ltd Sales +€1,889.00 Sep 24 WeWork Office −€2,268.00 Sep 23 Figma Software −€247.00 Sep 22 Stripe payout Sales +€626.00 Sep 21 Lufthansa Travel −€1,005.00 Sep 20 Northwind Traders Sales +€1,384.00 Sep 19 AWS Hosting −€1,763.00 Sep 18 Contoso Ltd Sales +€2,142.00 Sep 17 WeWork Office −€121.00 Sep 16 Figma Software −€500.00 Sep 15 Stripe payout Last reconciled Sales +€879.00 Sep 14 Lufthansa Travel −€1,258.00 Sep 13 Northwind Traders Sales +€1,637.00 Sep 12 AWS Hosting −€2,016.00 Sep 11 Contoso Ltd Sales +€2,395.00 Sep 10 WeWork Office −€374.00 Sep 9 Figma Software −€753.00 Sep 8 Stripe payout Sales +€1,132.00 Sep 7 Lufthansa Travel −€1,511.00 Sep 6 Northwind Traders Sales +€1,890.00 Sep 5 AWS Hosting −€2,269.00 Sep 4 Contoso Ltd Sales +€248.00 Sep 3 WeWork Office −€627.00 Sep 2 Figma Software −€1,006.00 Sep 1 Stripe payout Sales +€1,385.00 Шире экрана, а название на месте
Девять месяцев не помещаются, и таблица прокручивается вбок — со
sticky-columnназвание пространства остаётся у левого края, на непрозрачном фоне, чтобы числа не просвечивали.region-labelдаёт имя прокручиваемой области; клавиатура доходит до неё и без него.API requests by workspaceThousands per month, 2026API requests by workspace, thousands per month Workspace Jan Feb Mar Apr May Jun Jul Aug Sep Total Acme Billing 40 65 90 115 50 75 100 125 60 720 Acme Support 77 103 129 65 91 117 143 79 105 909 Northwind 114 51 78 105 132 69 96 123 150 918 Contoso Labs 61 89 117 55 83 111 139 77 105 837 Tailspin 98 127 66 95 124 153 92 121 150 1,026 Заказы, которые раскрываются в позиции
С
row-groupsтело ваше: по<tbody>на заказ держат заказ и его позиции вместе. Строка позиций помеченаdata-gr-table-off-grid— ей не достаются полоса и подсветка, а заказы ниже сохраняют чётность. Кнопку и раскрытую строку связывают вашиaria-expandedиaria-controls.Orders this week Lines Order Customer Placed Total #10482 Northwind Traders Oct 6 €1,218.00 2 × Ergonomic chair€578.001 × Standing desk€640.00#10481 Contoso Ltd Oct 6 €948.00 #10480 Tailspin Toys Oct 5 €292.00 #10479 Fabrikam Inc Oct 5 €1,180.00 #10478 Wide World Importers Oct 4 €1,734.00 #10477 Adventure Works Oct 4 €380.00 #10476 Litware Oct 3 €1,920.00 #10475 Proseware Oct 2 €280.00
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер таблицы — базовый кегль текста. Паддинги ячеек оставлены за консьюмером (GrTable — «тонкий» контейнер). |
ariaLabel | string | undefined | undefined | Прямой ARIA-label для `<table>`. Игнорируется, если задан `ariaLabelledby`. |
loading | boolean | undefined | false | Идёт загрузка: вместо строк — скелетоны, контейнер помечен `aria-busy`. |
regionLabel | string | undefined | undefined | ARIA-label для скролл-контейнера. Включает `role="region"`; сам скролл достижим с клавиатуры всегда, независимо от метки. |
hoverable | boolean | undefined | false | Подсветка строки под курсором. |
ariaLabelledby | string | undefined | undefined | ID элемента-заголовка, связанного с `<table>` через `aria-labelledby`. |
empty | boolean | undefined | undefined | Таблица пуста. По умолчанию определяется по содержимому слота. |
emptyText | string | undefined | undefined | Текст пустого состояния. Слот `#empty` сильнее. |
maxHeight | string | number | undefined | undefined | Максимальная высота скролл-контейнера (включает вертикальный скролл). Число трактуется как пиксели. Нужен для работы `stickyHeader`. |
caption | string | undefined | undefined | Текст caption для screen reader'ов. Рендерится как `<caption class="sr-only">`, если не передан слот `#caption`. |
stickyHeader | boolean | undefined | false | Прилипающий заголовок: `<thead>` остаётся видимым при вертикальном скролле. Осмысленно вместе с `maxHeight` (иначе таблица не скроллится вертикально). |
loadingRows | number | undefined | 3 | Сколько строк-заглушек показать при `loading`. |
role | "grid" | "table" | undefined | "table" | Роль таблицы. `grid` — интерактивная сетка с клавиатурой по ячейкам (паттерн WAI-ARIA `grid`): она объявляет диктору, что по таблице ходят стрелками, а не читают её как статичную. Роль ставит тот, кто клавиатуру и реализует, — сама по себе она обещание, которое таблица не выполняет. |
columnCount | number | undefined | 1 | Сколько колонок занимает служебная строка (пустое состояние и скелетоны). |
striped | boolean | undefined | false | Чередование строк. |
rowGroups | boolean | undefined | false | Тело собрано группами строк, и рисует их потребитель: слот по умолчанию встаёт прямо в таблицу, без обёртки в `<tbody>`. Нужно там, где пара строк обязана мериться и жить вместе — второй ярус `GrDataTable` рядом со своей строкой. Обернуть две `<tr>` в таблице больше не во что: группа строк — единственный контейнер, который для них существует. |
stickyColumn | boolean | undefined | false | Прилипающая первая колонка: остаётся видимой при горизонтальном скролле. Только первая, и это не упрощение: `left` со второй колонки равен сумме ширин предыдущих, а ячейки принадлежат потребителю — мерить их «тонкому» контейнеру нечем. Многоколоночное закрепление есть у `GrDataTable`: он знает свои колонки и меряет смещения сам. |
rowCount | number | undefined | undefined | Полное число строк набора, включая строки заголовка (`aria-rowcount`). Нужен, когда в DOM не весь набор — например, при виртуализации: диктор считает строки по разметке и объявил бы «5 из 20» на таблице в десять тысяч. Строки при этом обязаны нести `aria-rowindex`. |
fixedLayout | boolean | undefined | false | Фиксированная раскладка (`table-layout: fixed`): ширины колонок берутся из первой строки, а не из содержимого всех. Обязателен, если в DOM не весь набор: иначе ширины считаются по отрисованному окну и прыгают на каждой прокрутке. Класс — `table-fixed`. Раньше здесь стояло arbitrary-значение `[table-layout:fixed]`, потому что правила не было в `preset-mini`; в словаре `preset-wind3` оно есть. |
tableMinWidth | string | number | undefined | undefined | Минимальная ширина самой таблицы (число — пиксели). Нужна при `fixedLayout`: с фиксированной раскладкой и шириной `auto` браузер вписывает таблицу в контейнер и делит место между колонками пропорционально — то есть заданные ширины молча ужимаются, а горизонтальной прокрутки, на которой держатся закреплённые колонки, не возникает вовсе. Это нижняя граница, а не ширина: в контейнере шире суммы колонок таблица по-прежнему занимает его целиком. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Строки таблицы, когда разметка пишется руками вместо `data`. |
caption | any | Подпись таблицы — `<caption>`, читается диктором первой. |
header | any | Шапка вместо построенной по `columns`. |
loading | any | Содержимое, пока едут данные, — вместо строк-заглушек. |
empty | any | Пустое состояние вместо текста по умолчанию. |
footer | any | Итоговая строка под таблицей. |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
scrollTo | (options: ScrollToOptions) => void | Прокрутить скролл-контейнер таблицы — тот же контракт, что у `GrDataTable`. |
scrollToRow | (index: number, options?: ScrollIntoViewOptions | undefined) => boolean | Прокрутить к строке содержимого по её индексу в разметке. `false` — строки нет в DOM. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrTableProps, GrTableConfigurableProps, GrTableSize } from '@feugene/granularity/components/GrTable'Файлdist/types/components/GrTable/GrTable.vue.d.ts
interface GrTableProps {
/**
* Размер таблицы — базовый кегль текста. Паддинги ячеек оставлены за
* консьюмером (GrTable — «тонкий» контейнер).
*/
size?: GrTableSize;
/**
* Текст caption для screen reader'ов. Рендерится как `<caption class="sr-only">`,
* если не передан слот `#caption`.
*/
caption?: string;
/** Прямой ARIA-label для `<table>`. Игнорируется, если задан `ariaLabelledby`. */
ariaLabel?: string;
/** ID элемента-заголовка, связанного с `<table>` через `aria-labelledby`. */
ariaLabelledby?: string;
/**
* ARIA-label для скролл-контейнера. Включает `role="region"`; сам скролл
* достижим с клавиатуры всегда, независимо от метки.
*/
regionLabel?: string;
/** Идёт загрузка: вместо строк — скелетоны, контейнер помечен `aria-busy`. */
loading?: boolean;
/** Сколько строк-заглушек показать при `loading`. */
loadingRows?: number;
/** Сколько колонок занимает служебная строка (пустое состояние и скелетоны). */
columnCount?: number;
/** Текст пустого состояния. Слот `#empty` сильнее. */
emptyText?: string;
/** Таблица пуста. По умолчанию определяется по содержимому слота. */
empty?: boolean;
/** Чередование строк. */
striped?: boolean;
/** Подсветка строки под курсором. */
hoverable?: boolean;
/**
* Тело собрано группами строк, и рисует их потребитель: слот по умолчанию
* встаёт прямо в таблицу, без обёртки в `<tbody>`.
*
* Нужно там, где пара строк обязана мериться и жить вместе — второй ярус
* `GrDataTable` рядом со своей строкой. Обернуть две `<tr>` в таблице больше
* не во что: группа строк — единственный контейнер, который для них
* существует.
*/
rowGroups?: boolean;
/**
* Роль таблицы. `grid` — интерактивная сетка с клавиатурой по ячейкам
* (паттерн WAI-ARIA `grid`): она объявляет диктору, что по таблице ходят
* стрелками, а не читают её как статичную.
*
* Роль ставит тот, кто клавиатуру и реализует, — сама по себе она обещание,
* которое таблица не выполняет.
*/
role?: 'table' | 'grid';
/**
* Прилипающий заголовок: `<thead>` остаётся видимым при вертикальном скролле.
* Осмысленно вместе с `maxHeight` (иначе таблица не скроллится вертикально).
*/
stickyHeader?: boolean;
/**
* Прилипающая первая колонка: остаётся видимой при горизонтальном скролле.
*
* Только первая, и это не упрощение: `left` со второй колонки равен сумме
* ширин предыдущих, а ячейки принадлежат потребителю — мерить их «тонкому»
* контейнеру нечем. Многоколоночное закрепление есть у `GrDataTable`: он
* знает свои колонки и меряет смещения сам.
*/
stickyColumn?: boolean;
/**
* Полное число строк набора, включая строки заголовка (`aria-rowcount`).
*
* Нужен, когда в DOM не весь набор — например, при виртуализации: диктор
* считает строки по разметке и объявил бы «5 из 20» на таблице в десять
* тысяч. Строки при этом обязаны нести `aria-rowindex`.
*/
rowCount?: number;
/**
* Фиксированная раскладка (`table-layout: fixed`): ширины колонок берутся из
* первой строки, а не из содержимого всех.
*
* Обязателен, если в DOM не весь набор: иначе ширины считаются по
* отрисованному окну и прыгают на каждой прокрутке.
*
* Класс — `table-fixed`. Раньше здесь стояло arbitrary-значение
* `[table-layout:fixed]`, потому что правила не было в `preset-mini`; в
* словаре `preset-wind3` оно есть.
*/
fixedLayout?: boolean;
/**
* Минимальная ширина самой таблицы (число — пиксели).
*
* Нужна при `fixedLayout`: с фиксированной раскладкой и шириной `auto`
* браузер вписывает таблицу в контейнер и делит место между колонками
* пропорционально — то есть заданные ширины молча ужимаются, а
* горизонтальной прокрутки, на которой держатся закреплённые колонки, не
* возникает вовсе.
*
* Это нижняя граница, а не ширина: в контейнере шире суммы колонок таблица
* по-прежнему занимает его целиком.
*/
tableMinWidth?: string | number;
/**
* Максимальная высота скролл-контейнера (включает вертикальный скролл).
* Число трактуется как пиксели. Нужен для работы `stickyHeader`.
*/
maxHeight?: string | number;
}Файлdist/types/components/GrTable/config.d.ts
const grTableConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTable">;Файлdist/types/components/GrTable/defaults.d.ts
Пропы GrTable, настраиваемые глобально через componentDefaults.
interface GrTableConfigurableProps {
size: GrComponentSize;
}Файлdist/types/components/GrTable/grTableStyles.d.ts
type GrTableSize = GrComponentSize;Файлdist/types/components/GrTable/safelist.d.ts
Только полоса и подсветка строк: эти классы склеиваются шаблонной строкой из
атрибута служебной строки и оттенка (grTableStyles.ts: stripedClass,
hoverableClass, stickyColumnStripedClass, stickyColumnHoverableClass),
и целиком их нет ни в одной строке кода. Остальное — шаблон, мапа кеглей,
классы служебных строк — лежит литералами, и granum извлекает это из чанков
компонента сам, включая общие.
const grTableSafelist: string[];Файлdist/types/components/GrTable/index.d.ts
type GrTableInstance = ComponentExposed<typeof GrTableComponent>;Доступность
Паттерн APG
—
| Клавиша | Действие |
|---|---|
| — | при maxHeight или горизонтальном переполнении область прокрутки — таб-стоп (tabindex="0"), листается стрелками и PageUp/PageDown. С regionLabel она получает role="region" и имя: безымянную область скринридер объявляет просто «регион» |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Скролл достижим с клавиатуры
Контейнер overflow-x-auto всегда стоит в таб-порядке (tabindex="0").
Раньше это зависело от regionLabel: широкую таблицу без метки нельзя было
проскроллить с клавиатуры вовсе — прямое нарушение WCAG 2.1.1.
regionLabel по-прежнему включает role="region" и даёт области имя, но к
достижимости скролла отношения не имеет.
Липкая первая колонка
stickyColumn оставляет первую колонку на месте при горизонтальной прокрутке:
у широкой таблицы иначе видно числа, но не видно, чьи они. Со stickyHeader
складывается — держатся обе оси.
Только первая, и это не упрощение. Смещение left со второй колонки равно
сумме ширин предыдущих, а ячейки принадлежат вам: реестра ячеек и замеров у
«тонкого» контейнера нет. Многоколоночное закрепление есть у
GrDataTable — он знает свои колонки и меряет смещения сам.
Фон липкой ячейки непрозрачен, иначе сквозь неё видно уезжающее. Наследовать
фон строки не выйдет, хотя это и просится: оттенки striped и hoverable
полупрозрачны, и сквозь такую ячейку числа просвечивают — проверено глазами.
Поэтому те же оттенки собираются заново поверх карточки: цвет тот же, дыры нет.
Полосатость и подсветка в первой колонке работают наравне с остальными, ничего
дописывать не нужно.
Тень справа — признак, что под колонкой что-то уезжает. Рамка не годится: она
сдвинула бы сетку. Значение настраивается токеном
--gr-table-sticky-column-shadow.
Горизонтальной прокрутки min-w-full сам по себе не даёт: нужны tableMinWidth
либо white-space: nowrap на ячейках.
Пустое состояние и загрузка
<GrTable :loading="pending" :column-count="4">
<template #header>…</template>
<tr v-for="row in rows" :key="row.id">…</tr>
<template #empty>Ничего не найдено</template>
</GrTable>Пустоту таблица определяет сама — по содержимому слота. columnCount нужен
служебной строке: без него colspan не растянется на всю ширину.
loading рисует строки-скелетоны и помечает контейнер aria-busy; слот
#loading заменяет их целиком. Загрузка сильнее пустоты — иначе таблица мигала
бы текстом «пока пусто» на каждом запросе.
Императивный API
const table = ref<InstanceType<typeof GrTable>>()
table.value?.scrollToRow(42) // индекс строки содержимого; false — строки нет
table.value?.scrollTo({ top: 0 })scrollToRow(index, options?) принимает индекс строки в разметке — строки
таблица не рендерит и ключей у них не знает, поэтому адресация только
позиционная. Считаются строки содержимого: служебные skeleton- и empty-строки
не адресуются, в состояниях loading и «пусто» метод возвращает false, как и
при индексе вне диапазона. У GrDataTable одноимённый метод работает по
ключу строки — при миграции аргумент меняет смысл.
Оформление строк
striped и hoverable вешаются на таблицу целиком, а не на каждую ячейку:
разметку строк потребитель пишет сам, и требовать от него классов было бы
странно. Не на <tbody> — групп строк в теле может быть много (см. ниже), и
правило, привязанное к одной, не увидело бы чередование между ними.
Sticky-заголовок использует локальный z-[1], липкая колонка — z-[2], оба
внутри собственного контейнера —
к шкале --gr-z-* он отношения не имеет, это описано в
../z-index.md.
Строка вне набора: подробности, распорки
Полосатость и подсветка написаны от <tbody> и считают прямых потомков.
Дописав к строке вторую — раскрытые подробности, служебную распорку, — вы
сдвинули бы чётность у всех строк ниже и получили бы подсветку под курсором
там, где нажимать не на что.
Пометьте такую строку data-gr-table-off-grid:
<tr>
<td>Заказ 42</td>
</tr>
<tr data-gr-table-off-grid>
<td colspan="3">Подробности заказа</td>
</tr>Атрибут снимает с неё оба правила и выключает её из счёта чётности:
селектор написан как nth-child(even of :not([data-gr-table-off-grid])), а не
просто с фильтром — обычный :not() убрал бы полосу с самой строки, но
нумерацию соседей не изменил.
Связывать пару строк для скринридера — тоже ваша забота: кнопке нужен
aria-expanded и aria-controls на id строки подробностей. Готовое
раскрытие со своим состоянием, подгрузкой и ARIA есть у
GrDataTable — здесь только оформление.
Тело группами строк
rowGroups отдаёт тело потребителю целиком: слот по умолчанию встаёт прямо в
таблицу, без обёртки в <tbody>, и группы рисуете вы.
Нужно это там, где пара строк обязана жить и мериться вместе. Обернуть две
<tr> в таблице больше не во что — группа строк единственный контейнер, который
для них существует, — и без неё виртуализатор GrDataTable
не видел бы высоту второго яруса раскрытой строки.
<GrTable row-groups>
<tbody v-for="order in orders" :key="order.id">
<tr><td>{{ order.title }}</td></tr>
<tr v-if="isOpen(order)" data-gr-table-off-grid>
<td colspan="3">Подробности</td>
</tr>
</tbody>
</GrTable>Полосатость это переживает: селектор чередования написан в двух формах — по
строкам внутри единственной группы и по самим группам, — и в чужой форме каждая
не совпадает ни с чем. Группу вне набора (распорку) помечайте тем же
data-gr-table-off-grid, что и строку.
Загрузка и пустота остаются за таблицей: их она рисует сама и в своей группе.
Неполный набор строк в разметке
Два пропа нужны, когда строки рендерит не всё содержимое набора — например, при
виртуализации в GrDataTable:
rowCount— полное число строк вместе с заголовочными, уходит вaria-rowcount. Без него диктор считает строки по разметке и объявит «5 из 20» на таблице в десять тысяч; строки при этом обязаны нестиaria-rowindex.fixedLayout—table-layout: fixed. Без него ширины колонок считаются по отрисованному окну и прыгают на каждой прокрутке.
rowCount заодно выключает браузерный якорь прокрутки у скролл-контейнера: он
подправляет scrollTop, когда меняется высота содержимого выше видимого узла, —
а окно меняет её на каждом кадре, и список уезжает тем дальше, чем грубее оценка
строки.
Пропы этого примера