GrCard
Берут, когда контенту нужна поверхность.
Обзор
Когда брать
- контенту нужна поверхность — рамка, фон, радиус и тень одним компонентом вместо своих классов;
- у блока есть заголовок —
titleдаёт настоящийh2…h6, по которому отчёт из шести карточек обходится структурой; - карточка кликабельна —
clickable/hrefдают роль и клавиатуру целиком, а не@clickнаdiv; - карточек много — общий вид держится
variant, а не копией классов в каждом месте; - отступы задаются раскладкой —
paddingвключаяnoneдля таблицы или картинки край в край.
Когда взять другое
| Нужно | Берите |
|---|---|
| Нужна структура шапки, тела и подвала окна | GrDialog |
| Карточка — виджет на сетке дашборда | GrDashboardItem |
| Карточка объясняет пустоту | GrEmptyState |
| Внутри список строк | GrList |
| Нужен только разделитель | GrDivider |
<GrCard> без пропов рендерит один <div> со слотом: ни обёрток, ни
отступов. Это позволяет ставить его под любую раскладку, не борясь с чужими
полями.
Варианты все тона и виды — настоящим компонентом
variant
elevatedoutlinedghost
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrCard
title="Workspace"
description="Billing and access"
padding="md"
>
Three projects, twelve members.
</GrCard>Примеры 5 у каждого — живое демо и его код
Вся анатомия в одной карточке
Заголовок и описание — пропы, а не разметка: карточка печатает настоящий заголовок над приглушённой строкой.
actionsставит статус рядом с ним, не отнимая этот заголовок,body-classраскладывает тело, аfooterдержит то, что с карточкой делают: кнопки в теле спорили бы с содержимым за внимание.Team plan
€48 per seat a month, billed yearly
ActiveSeats9 of 12Storage182 GB of 500 GBAutomation runs8,740 of 10,000Renews on 12 March 2027Кнопки внутри, ссылка в заголовке
Карточка со своими кнопками не может быть ссылкой: интерактив внутри интерактива ломает клавиатуру и скринридер.
hoverableподсвечивает поверхность, не делая её интерактивной, слотheaderнесёт заголовок со ссылкой, аas="article"помечает каждую карточку самостоятельной записью.GWRetry webhook deliveries with backoff
#4182 · Grace Wu · 2 hours ago
Failed deliveries are retried five times with exponential backoff instead of being dropped after the first 5xx.
backendreliability+142−38 · 6 filesLRMove invoice PDFs to the EU bucket
#4179 · Linus Ray · yesterday
New invoices are written to eu-central-1; a one-off job migrates the 18 400 existing files.
compliance+61−12 · 3 filesТаблица край в край
Без
paddingу тела нет полей, и таблица идёт от рамки до рамки, а шапка, которую карточка рисует сама, свои поля сохраняет.as="section"сregion-labelделает карточку именованной областью страницы, аheading-levelвписывает заголовок в структуру страницы.Recent invoices
Last 30 days · 128 invoices
Invoice Customer Issued Amount Status INV-2041 Northwind Traders 2 Oct 2026 €4,320.00 Paid INV-2040 Contoso Ltd 29 Sep 2026 €12,850.00 Due INV-2039 Fabrikam Inc 24 Sep 2026 €960.00 Overdue INV-2038 Tailspin Toys 19 Sep 2026 €2,475.50 Paid Showing 4 of 128View all invoicesВся карточка — ссылка или кнопка
С
hrefкарточка — одна ссылка и одна остановка табуляции, с именем из заголовка; сclickable— кнопка, которая отдаётclick. Сетка карточек берётvariantиpaddingодин раз изGrConfigProvider, а не повторяет их на каждой.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
variant | GrCardVariant | undefined | undefined | `elevated` — рамка, фон и тень; `outlined` — без тени; `ghost` — без рамки. |
title | string | undefined | undefined | Заголовок карточки — настоящий `h2`…`h6`, а не жирная строка: отчёт из шести карточек иначе не обойти по структуре, после `h1` страницы в нём нет ни одного заголовка. |
as | string | Component | undefined | undefined | Свой корневой тег: компонент-ссылка (`RouterLink`, `Link` от Inertia) или семантический тег страницы (`section`, `article`, `aside`). Сильнее `href`. Неинтерактивный тег — только замена `div`: ни подсветки, ни кольца фокуса. |
href | string | undefined | undefined | Карточка-ссылка. |
padding | GrCardPadding | undefined | undefined | Внутренние отступы. По умолчанию `none`: карточка — поверхность, а её содержимое (`GrCollapse`, `GrList`) само знает про свои отступы. |
regionLabel | string | undefined | undefined | Имя карточки как области страницы: `role="region"` плюс `aria-label`. Безымянная `<section>` для скринридера — обычный контейнер, и это норма: именованных областей на страницу нужно немного, иначе их обзор перестаёт помогать. Интерактивной карточке не даётся — `role="region"` на `<button>` невалиден. |
clickable | boolean | undefined | false | Карточка-кнопка: интерактивна вся поверхность. |
hoverable | boolean | undefined | false | Подсветка при наведении без интерактивности. |
bodyClass | string | undefined | undefined | Классы обёртки тела — она появляется вместе с секциями. |
description | string | undefined | undefined | Пояснение под заголовком. |
headingLevel | 2 | 3 | 4 | 5 | 6 | undefined | undefined | Уровень заголовка под структуру страницы. Не задан — из `GrConfigProvider`, иначе `3`. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Содержимое карточки. |
header | any | Шапка вместо пропов `title` и `description`. |
actions | any | Действия справа в собственной шапке карточки: «⋯», «Обновить», переключатель периода. Включают шапку так же, как `title`. Со слотом `#header` не сочетается: тот заменяет шапку целиком, и всё, что в ней есть, рисует потребитель. |
footer | any | Подвал: действия, сводка, пагинация. |
События
| Event | Сигнатура | Описание |
|---|---|---|
click | (event: "click", event: MouseEvent): void | — |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrCardProps, GrCardEmits, GrCardConfigurableProps } from '@feugene/granularity/components/GrCard'Файлdist/types/components/GrCard/GrCard.vue.d.ts
interface GrCardProps {
/**
* Внутренние отступы. По умолчанию `none`: карточка — поверхность, а её
* содержимое (`GrCollapse`, `GrList`) само знает про свои отступы.
*/
padding?: GrCardPadding;
/** `elevated` — рамка, фон и тень; `outlined` — без тени; `ghost` — без рамки. */
variant?: GrCardVariant;
/**
* Свой корневой тег: компонент-ссылка (`RouterLink`, `Link` от Inertia) или
* семантический тег страницы (`section`, `article`, `aside`). Сильнее `href`.
* Неинтерактивный тег — только замена `div`: ни подсветки, ни кольца фокуса.
*/
as?: string | Component;
/** Карточка-ссылка. */
href?: string;
/**
* Имя карточки как области страницы: `role="region"` плюс `aria-label`.
* Безымянная `<section>` для скринридера — обычный контейнер, и это норма:
* именованных областей на страницу нужно немного, иначе их обзор перестаёт
* помогать. Интерактивной карточке не даётся — `role="region"` на `<button>`
* невалиден.
*/
regionLabel?: string;
/** Карточка-кнопка: интерактивна вся поверхность. */
clickable?: boolean;
/** Подсветка при наведении без интерактивности. */
hoverable?: boolean;
/** Классы обёртки тела — она появляется вместе с секциями. */
bodyClass?: string;
/**
* Заголовок карточки — настоящий `h2`…`h6`, а не жирная строка: отчёт из
* шести карточек иначе не обойти по структуре, после `h1` страницы в нём
* нет ни одного заголовка.
*/
title?: string;
/** Пояснение под заголовком. */
description?: string;
/** Уровень заголовка под структуру страницы. Не задан — из `GrConfigProvider`, иначе `3`. */
headingLevel?: GrCardHeadingLevel;
}interface GrCardEmits {
/** Клик по корню карточки. С клавиатуры приходит только у `clickable` и карточки-ссылки. */
(e: 'click', event: MouseEvent): void;
}Файлdist/types/components/GrCard/config.d.ts
const grCardConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCard">;Файлdist/types/components/GrCard/defaults.d.ts
Пропы GrCard, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrCard: { … } }">.
Только оформление — см. GrButton/defaults.ts.
interface GrCardConfigurableProps {
padding: GrCardPadding;
variant: GrCardVariant;
headingLevel: GrCardHeadingLevel;
}Файлdist/types/components/GrCard/grCardStyles.d.ts
type GrCardPadding = 'none' | 'sm' | 'md' | 'lg';type GrCardVariant = 'elevated' | 'outlined' | 'ghost';type GrCardHeadingLevel = typeof GR_CARD_HEADING_LEVELS[number];Файлdist/types/components/GrCard/safelist.d.ts
const grCardSafelist: string[];Использование из прозы библиотеки
Дефолт — одна поверхность и ничего сверх
<GrCard> без пропов рендерит один <div> со слотом внутри: ни обёрток, ни
отступов. Это не осторожность ради осторожности — на GrCard стоят
GrCollapse и GrList, и сдвинутый дефолт поехал бы у них. Отсюда
padding="none" по умолчанию.
Отступы
<GrCard padding="md">
…
</GrCard>none (по умолчанию), sm (12px), md (16px), lg (24px). Пока секций нет,
отступ живёт на самой поверхности — лишней обёртки не появляется.
Варианты
| Значение | Что даёт |
|---|---|
elevated (по умолчанию) | рамка + фон + тень |
outlined | рамка + фон, без тени |
ghost | только фон — карточка внутри карточки не рисует вторую рамку |
padding и variant читаются из GrConfigProvider:
<GrConfigProvider :component-defaults="{ GrCard: { padding: 'md', variant: 'outlined' } }">Секции
<GrCard padding="md">
<template #header>
<h3>Релиз 0.15</h3>
</template>
Тело карточки.
<template #footer>
<GrButton size="sm">Опубликовать</GrButton>
</template>
</GrCard>Шапка отбивается border-b, подвал — border-t. Как только появляется хотя бы
одна секция (или bodyClass), отступ переезжает с поверхности на секции —
иначе он сложился бы вдвое.
Карточка отбивает то, что рисует сама. Шапка из title — её собственная, и
отступ ей полагается независимо от padding, включая дефолтный none: иначе
заголовок и разделитель под ним оказываются прижаты к рамке. Это и есть типовая
карточка отчёта — заголовок с отступом, таблица край в край:
<GrCard title="Документы" variant="outlined">
<GrDataTable :rows="rows" :columns="columns" />
</GrCard>Слоты #header и #footer наполняет потребитель — их отступы идут из padding
и остаются его заботой. Правило то же, что и у приоритета: #header сильнее
пропов и заменяет шапку целиком, а вместе с ней и ответственность за отступы.
Иначе потребитель, у которого внутри слота свои отступы, получил бы двойные.
bodyClass кладёт классы на обёртку тела — там же, где обычно нужен
grid gap-*.
Заголовок — настоящий, а не жирная строка
<GrCard title="Выручка за месяц" description="Без учёта возвратов" padding="md">
<RevenueChart />
</GrCard>title печатается тегом h2…h6 (headingLevel, по умолчанию h3), а не
<span class="font-semibold">. Разница не косметическая: страница из шести
карточек, где после h1 нет ни одного заголовка, обходится скринридером
только подряд — прыгать между блоками нечем.
#header сильнее пропов: слот занят — title и description не
рендерятся вовсе. Нестандартная шапка не обязана объяснять, почему она не
title.
Действие рядом с заголовком — слот #actions, и шапка при этом остаётся
своей: заголовок печатается тем же h2…h6, отступы берутся из шкалы карточки.
<GrCard title="Продажи за неделю" :heading-level="3">
<template #actions>
<GrButton size="xs" variant="ghost">Обновить</GrButton>
</template>
</GrCard>Слот появился не ради удобства: третьего случая не было — либо заголовок из
пропов, либо #header целиком, — и любая кнопка в шапке заставляла забрать
слот и переписать заголовок вручную. К этому обходу независимо пришли
GrDashboardItem в кольце и обёртки потребителей, и все теряли одно и то же:
настоящий уровень заголовка и шкалу отступов.
С #header слот не сочетается: тот заменяет шапку целиком, включая место, куда
действия встали бы. GrDashboardItem по-прежнему пользуется #header — у него
левее заголовка живёт ручка перетаскивания, а её #actions не выражает.
Карточка-ссылка берёт доступное имя из заголовка (aria-labelledby), а не из
всего содержимого подряд.
У кликабельной карточки заголовка не будет. Корень там — <button>, а его
контент-модель это phrasing content: <h3> внутри невалиден. title
печатается <span>, и в dev компонент об этом предупреждает. Нужен и
заголовок, и переход — hoverable плюс ссылка в самом заголовке; это тот же
приём, что и с действиями внутри (ниже).
Карточка-ссылка и карточка-кнопка
<GrCard href="/reports/42" padding="md">
…
</GrCard>
<GrCard clickable padding="md" @click="open">
…
</GrCard>
<GrCard :as="RouterLink" :to="{ name: 'report' }" padding="md">
…
</GrCard>Порядок выбора тега — as → <a href> → <button clickable> → <div>, как у
GrLink и GrListItem. Интерактивная карточка получает кольцо фокуса и
подсветку при наведении.
Интерактивность даёт разрешённый тег, а не сам факт as. Кольцо фокуса
достаётся тому, кто умеет фокус: <button>, <a> со ссылкой и компоненту
роутера, про который до рендера известно только то, что он рендерит <a>.
as="section" не делает карточку интерактивной — см. ниже.
Ограничение: внутрь кликабельной карточки нельзя класть кнопки и ссылки —
интерактив внутри интерактива ломает и клавиатуру, и скринридер (axe:
nested-interactive). Карточке с действиями внутри нужен hoverable плюс
ссылка в заголовке — визуально это то же самое, семантически — правильно.
hoverable подсвечивает карточку, не делая её интерактивной.
Семантика страницы: as — это замена тега
Страница, у которой блок обязан быть <section> или <article> по разметке
документа, берёт as со строковым тегом:
<GrCard as="section" title="Продажи" padding="md">
…
</GrCard>Меняется только тег. Классы, отступы, заголовок и слоты — те же, что у
<div>: неинтерактивный тег не получает ни подсветки под курсором, ни кольца
фокуса, ни aria-labelledby. Карточка остаётся поверхностью, а разметка
страницы становится честной.
<div> остаётся дефолтом намеренно. HTML-спека не советует section там, где
элемент нужен ради оформления, а карточка чаще всего именно поверхность —
GrCollapse, GrList и GrSortableList стоят на ней как раз в этом качестве.
Тег выбирает страница, которая знает свою структуру, а не компонент.
Обратная сторона правила: clickable или href вместе с тегом, который не
попадает в таб-порядок, — ошибка вёрстки. Карточка будет кликаться мышью и
останется недостижимой с клавиатуры, и в dev компонент об этом предупреждает.
Карточка как область страницы
Безымянная <section> для скринридера — обычный контейнер, и это норма. Имя
превращает её в лендмарк, а именованных областей на страницу нужно немного:
десяток карточек-регионов делает их обзор бесполезным. Поэтому имя даётся по
явной просьбе — regionLabel:
<GrCard as="section" region-label="Продажи за август" title="Продажи" padding="md">
…
</GrCard>Проп ставит role="region" и aria-label — тот же приём, что у
GrTable и GrFormSection.
Интерактивной карточке он не даётся и в dev предупреждает: role="region"
поверх <button> невалиден, а имя ссылке уже дал заголовок.
Пропы этого примера