GrTable

  • ядро
  • Данные
  • API · 20 props
  • ⌨ клавиши

Берут, когда ячейки оформляет потребитель.

import { GrTable } from '@feugene/granularity/components/GrTable'

Обзор

Когда брать

  • ячейки оформляет потребитель — своя разметка <tr>/<td>, а от компонента нужны только обвязка и поведение;
  • таблица шире экрана — скролл-обёртка достижима с клавиатуры и объявлена как регион;
  • заголовок должен оставаться видимым — stickyHeader вместе с maxHeight;
  • колонка-подпись должна оставаться видимой — stickyColumn при горизонтальной прокрутке;
  • таблица уже есть и её не переписывают — обёртка добавляет caption, состояния и скролл, не трогая разметку.

Когда взять другое

НужноБерите
Нужны сортировка, выбор строк, слоты ячеекGrDataTable
Строки однородные и без столбцовGrList
Строки вложеныGrTree
Порядок строк меняет пользовательGrSortableList
Данные показывают графикомGrChartBar

caption — не украшение: без него таблица в списке ориентиров скринридера безымянна, и на странице с тремя таблицами их не различить.

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 5 у каждого — живое демо и его код

  • Разметка ваша, рамка таблицы

    Строки пишете вы, поэтому в ячейке может быть что угодно — две строки текста, бейдж, просроченная дата красным. Таблица даёт рамку, тонированную шапку, подсветку строки и caption для диктора; #footer ставит итог в <tfoot>, ровно под его колонку.

    InvoicesOctober 2026 · 4 invoices
    Invoices, October 2026
    CustomerStatusDueAmount
    Northwind TradersINV-0421 · [email protected]
    SentOct 21€1,284.50
    Contoso LtdINV-0420 · [email protected]
    OverdueOct 2€432.00
    Tailspin ToysINV-0419 · [email protected]
    PaidSep 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 reconcile
    Transactions, business account
    DatePayeeCategoryAmount
    Oct 6Stripe payout Sales+€120.00
    Oct 5Lufthansa Travel−€499.00
    Oct 4Northwind Traders Sales+€878.00
    Oct 3AWS Hosting−€1,257.00
    Oct 2Contoso Ltd Sales+€1,636.00
    Oct 1WeWork Office−€2,015.00
    Sep 30Figma Software−€2,394.00
    Sep 29Stripe payout Sales+€373.00
    Sep 28Lufthansa Travel−€752.00
    Sep 27Northwind Traders Sales+€1,131.00
    Sep 26AWS Hosting−€1,510.00
    Sep 25Contoso Ltd Sales+€1,889.00
    Sep 24WeWork Office−€2,268.00
    Sep 23Figma Software−€247.00
    Sep 22Stripe payout Sales+€626.00
    Sep 21Lufthansa Travel−€1,005.00
    Sep 20Northwind Traders Sales+€1,384.00
    Sep 19AWS Hosting−€1,763.00
    Sep 18Contoso Ltd Sales+€2,142.00
    Sep 17WeWork Office−€121.00
    Sep 16Figma Software−€500.00
    Sep 15Stripe payout Last reconciledSales+€879.00
    Sep 14Lufthansa Travel−€1,258.00
    Sep 13Northwind Traders Sales+€1,637.00
    Sep 12AWS Hosting−€2,016.00
    Sep 11Contoso Ltd Sales+€2,395.00
    Sep 10WeWork Office−€374.00
    Sep 9Figma Software−€753.00
    Sep 8Stripe payout Sales+€1,132.00
    Sep 7Lufthansa Travel−€1,511.00
    Sep 6Northwind Traders Sales+€1,890.00
    Sep 5AWS Hosting−€2,269.00
    Sep 4Contoso Ltd Sales+€248.00
    Sep 3WeWork Office−€627.00
    Sep 2Figma Software−€1,006.00
    Sep 1Stripe payout Sales+€1,385.00
  • Шире экрана, а название на месте

    Девять месяцев не помещаются, и таблица прокручивается вбок — со sticky-column название пространства остаётся у левого края, на непрозрачном фоне, чтобы числа не просвечивали. region-label даёт имя прокручиваемой области; клавиатура доходит до неё и без него.

    API requests by workspaceThousands per month, 2026
    API requests by workspace, thousands per month
    WorkspaceJanFebMarAprMayJunJulAugSepTotal
    Acme Billing406590115507510012560720
    Acme Support77103129659111714379105909
    Northwind11451781051326996123150918
    Contoso Labs6189117558311113977105837
    Tailspin981276695124153921211501,026
  • Заказы, которые раскрываются в позиции

    С row-groups тело ваше: по <tbody> на заказ держат заказ и его позиции вместе. Строка позиций помечена data-gr-table-off-grid — ей не достаются полоса и подсветка, а заказы ниже сохраняют чётность. Кнопку и раскрытую строку связывают ваши aria-expanded и aria-controls.

    Orders this week
    LinesOrderCustomerPlacedTotal
    #10482Northwind TradersOct 6€1,218.00
    2 × Ergonomic chair€578.00
    1 × Standing desk€640.00
    #10481Contoso LtdOct 6€948.00
    #10480Tailspin ToysOct 5€292.00
    #10479Fabrikam IncOct 5€1,180.00
    #10478Wide World ImportersOct 4€1,734.00
    #10477Adventure WorksOct 4€380.00
    #10476LitwareOct 3€1,920.00
    #10475ProsewareOct 2€280.00

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
size"xs" | "sm" | "md" | "lg" | undefinedundefinedРазмер таблицы — базовый кегль текста. Паддинги ячеек оставлены за консьюмером (GrTable — «тонкий» контейнер).
ariaLabelstring | undefinedundefinedПрямой ARIA-label для `<table>`. Игнорируется, если задан `ariaLabelledby`.
loadingboolean | undefinedfalseИдёт загрузка: вместо строк — скелетоны, контейнер помечен `aria-busy`.
regionLabelstring | undefinedundefinedARIA-label для скролл-контейнера. Включает `role="region"`; сам скролл достижим с клавиатуры всегда, независимо от метки.
hoverableboolean | undefinedfalseПодсветка строки под курсором.
ariaLabelledbystring | undefinedundefinedID элемента-заголовка, связанного с `<table>` через `aria-labelledby`.
emptyboolean | undefinedundefinedТаблица пуста. По умолчанию определяется по содержимому слота.
emptyTextstring | undefinedundefinedТекст пустого состояния. Слот `#empty` сильнее.
maxHeightstring | number | undefinedundefinedМаксимальная высота скролл-контейнера (включает вертикальный скролл). Число трактуется как пиксели. Нужен для работы `stickyHeader`.
captionstring | undefinedundefinedТекст caption для screen reader'ов. Рендерится как `<caption class="sr-only">`, если не передан слот `#caption`.
stickyHeaderboolean | undefinedfalseПрилипающий заголовок: `<thead>` остаётся видимым при вертикальном скролле. Осмысленно вместе с `maxHeight` (иначе таблица не скроллится вертикально).
loadingRowsnumber | undefined3Сколько строк-заглушек показать при `loading`.
role"grid" | "table" | undefined"table"Роль таблицы. `grid` — интерактивная сетка с клавиатурой по ячейкам (паттерн WAI-ARIA `grid`): она объявляет диктору, что по таблице ходят стрелками, а не читают её как статичную. Роль ставит тот, кто клавиатуру и реализует, — сама по себе она обещание, которое таблица не выполняет.
columnCountnumber | undefined1Сколько колонок занимает служебная строка (пустое состояние и скелетоны).

Слоты

SlotTypeОписание
defaultanyСтроки таблицы, когда разметка пишется руками вместо `data`.
captionanyПодпись таблицы — `<caption>`, читается диктором первой.
headeranyШапка вместо построенной по `columns`.
loadinganyСодержимое, пока едут данные, — вместо строк-заглушек.
emptyanyПустое состояние вместо текста по умолчанию.
footeranyИтоговая строка под таблицей.

Методы

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

GrTablePropsinterface
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

grTableConfigconst
const grTableConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTable">;

Файлdist/types/components/GrTable/defaults.d.ts

GrTableConfigurablePropsinterface

Пропы GrTable, настраиваемые глобально через componentDefaults.

interface GrTableConfigurableProps {
    size: GrComponentSize;
}

Файлdist/types/components/GrTable/grTableStyles.d.ts

GrTableSizetype
type GrTableSize = GrComponentSize;

Файлdist/types/components/GrTable/safelist.d.ts

grTableSafelistconst

Только полоса и подсветка строк: эти классы склеиваются шаблонной строкой из атрибута служебной строки и оттенка (grTableStyles.ts: stripedClass, hoverableClass, stickyColumnStripedClass, stickyColumnHoverableClass), и целиком их нет ни в одной строке кода. Остальное — шаблон, мапа кеглей, классы служебных строк — лежит литералами, и granum извлекает это из чанков компонента сам, включая общие.

const grTableSafelist: string[];

Файлdist/types/components/GrTable/index.d.ts

GrTableInstancetype
type GrTableInstance = ComponentExposed<typeof GrTableComponent>;

Доступность

Паттерн APG

—

КлавишаДействие
—при maxHeight или горизонтальном переполнении область прокрутки — таб-стоп (tabindex="0"), листается стрелками и PageUp/PageDown. С regionLabel она получает role="region" и имя: безымянную область скринридер объявляет просто «регион»

Использование из прозы библиотеки

Скролл достижим с клавиатуры

Контейнер 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 на ячейках.

Пустое состояние и загрузка
vue
<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
ts
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:

vue
<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 не видел бы высоту второго яруса раскрытой строки.

vue
<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, когда меняется высота содержимого выше видимого узла, — а окно меняет её на каждом кадре, и список уезжает тем дальше, чем грубее оценка строки.