GrButton
Берут, когда действие меняет состояние.
Обзор
Когда брать
- действие меняет состояние — сохранить, удалить, отправить: адрес при этом не меняется;
- действие идёт на сервер —
loadingблокирует повтор и объявляет ожидание; - важность действия видна —
variantиtoneразводят главное, второстепенное и опасное; - кнопка только из иконки —
squareвместе сariaLabel: без имени такая кнопка безымянна; - нужна ссылка с видом кнопки —
href/asподменяют тег, оставляя семантику ссылки.
Когда взять другое
| Нужно | Берите |
|---|---|
| Переход меняет адрес | GrLink |
| Действий несколько в ряд | GrButtonGroup |
| Действий много, они прячутся | GrDropdownMenu |
| Переключается состояние «включено/выключено» | GrSwitch |
| Переключается вид одного содержимого | GrSegmented |
Варианты все тона и виды — настоящим компонентом
tone \ variant | primary | secondary | outline | ghost | ghost-border |
|---|---|---|---|---|---|
primary | |||||
neutral | |||||
success | |||||
warning | |||||
danger | |||||
info | |||||
slate | |||||
azure |
Размеры
xssmmdlg
Состояния
обычнаяdisabledloading
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrButton>Save</GrButton>Примеры 4 у каждого — живое демо и его код
Панель с одним громким действием
Второстепенные инструменты —
variant="ghost-border": тихие в ряду, с рамкой под указателем. Слотыprefixиsuffixдержат иконку, число фильтров и сочетание клавиш, и подпись с ними не сталкивается. Квадратная (square) кнопка из одной иконки получает имя изaria-label; залита только та, что создаёт.InvoicesNorthwind TradersINV-2026-0418 · Overdue€5,166.50Contoso LtdINV-2026-0417 · Paid€820.00Tailspin ToysINV-2026-0416 · Sent€310.00Сохранить и отменить в подвале формы
type="submit"иtype="reset"отдают работу форме: Enter сохраняет, а сброс перехватывается и возвращает сохранённое. Пока идёт запрос,loadingоставляет кнопку в фокусе и не пускает второй клик, аloading-textговорит скринридеру, что сохраняется. Обе кнопки ждут изменений — пока сохранять и отменять нечего.Разрушительное действие — через осознанный шаг
tone="danger"помечает то, что не отменить, а кнопка с контуром только открывает шаг. Последняя кнопка остаётсяdisabled, пока не набрано имя, занимает всю ширину черезblockи показываетloading, пока пространство удаляется. Отмена возвращает клавиатуру на кнопку-триггер через открытый методfocus().Danger zone Transfer ownershipHand the workspace to another admin.Delete workspaceInvoices, customers and 12 members lose access.Кнопки, которые на самом деле ссылки
Каждый шаг куда-то ведёт, поэтому каждая кнопка — ссылка с видом кнопки.
asберёт ссылку роутера для маршрута внутри приложения,hrefрисует обычный<a>, именованныйtargetсrelпереиспользует одну вкладку документации, аexternalсам открывает новую вкладку с безопаснымrel. Ссылка сdisabledуходит из порядка Tab и остаётся приглушённой — нативногоdisabledу ссылок нет.Launch checklistEvery step goes somewhere, so every button here is a link. - 1. Invite your teamInvite people
- 2. Read the getting-started guideOpen the guide
- 3. Look through the sourceGitHub(откроется в новой вкладке)
- 4. Connect a payment providerOnly an owner can do this.Connect
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | Действие недоступно: нативный `disabled` у `<button>`, у кнопки-ссылки — `aria-disabled` и снятый адрес. Цвета — из токенов, без прозрачности. |
type | "button" | "submit" | "reset" | undefined | "button" | Нативный `type` у `<button>`. По умолчанию `button`: кнопка в форме не отправляет её сама. |
tone |
| undefined | Смысл действия: цвет заливки, рамки и текста. `danger` — для необратимого, `neutral` — для второстепенного. По умолчанию `primary`; внутри `GrButtonGroup` — тон группы. |
variant | GrButtonVariant | undefined | undefined | Вес кнопки: `primary` — заливка тона, `secondary` — мягкая подложка с рамкой, `outline` — только рамка, `ghost` — только текст, `ghost-border` — текст, рамка которого проявляется под указателем. По умолчанию `primary`; глобально — `componentDefaults`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Высота и кегль по шкале контролов. По умолчанию `md`; берётся из группы и `GrConfigProvider`. |
ariaLabel | string | undefined | undefined | Доступное имя кнопки, когда текста нет: `square` с одной иконкой без него безымянна. |
loading | boolean | undefined | false | Действие выполняется: спиннер на месте префикса, а без него — поверх подписи, ширина не меняется; `aria-busy` и перехват клика. Кнопка при этом остаётся в фокусе — нативный `disabled` не ставится. |
loadingText | string | undefined | undefined | i18n: что именно грузится. `aria-busy` сам по себе часть AT не объявляет. |
square | boolean | undefined | undefined | Равные стороны — кнопка из одной иконки. Без текста ей нужен `ariaLabel`. |
as | string | Component | undefined | undefined | Полиморфизм: кастомный корневой тег/компонент (например, RouterLink). |
loadingIndicator | GrButtonLoadingIndicator | undefined | undefined | Вид загрузки. `spinner` — спиннер; `shimmer` — свет проходит по подписи, а подпись, иконки и ширина остаются на месте. Без текстовой подписи (кнопка из иконки) мерцать нечему — там спиннер. `track` — отрезок бежит по нижней кромке, содержимое на месте. По умолчанию `spinner`; тема меняет его через `componentDefaults` («Глина» — `shimmer`, «Чертёж» — `track`). |
prefixCell | boolean | undefined | undefined | Префикс в квадратной ячейке высотой с кнопку, за перегородкой. Только с подписью: у квадратной кнопки ячейка и есть кнопка. По умолчанию `false`; тема включает её через `componentDefaults` («Чертёж»). |
block | boolean | undefined | false | Кнопка на всю ширину контейнера. |
href | string | undefined | undefined | Рендерит кнопку как `<a href>` (если не задан `as`). |
target | string | undefined | undefined | `target` кнопки-ссылки. С `_blank` сам добавляется `rel="noopener noreferrer"`. |
rel | string | undefined | undefined | `rel` кнопки-ссылки. Без него при `target="_blank"` ставится `noopener noreferrer`. |
external | boolean | undefined | false | Открывать ссылку в новой вкладке: `target="_blank"` и `rel="noopener noreferrer"`. Уход объявляется заранее — значком справа и скрытым текстом в имени. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Содержимое кнопки. |
prefix | any | Аддон слева: иконка, счётчик. В состоянии загрузки уступает место спиннеру. |
suffix | any | Аддон справа: шеврон, счётчик. |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
focus | () => void | Поставить фокус на кнопку — и когда корень тег, и когда компонент из `as`. |
blur | () => void | Снять фокус с кнопки. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrButtonConfigurableProps, GrButtonVariant, GrButtonLoadingIndicator } from '@feugene/granularity/components/GrButton'Файлdist/types/components/GrButton/config.d.ts
const grButtonConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrButton">;Файлdist/types/components/GrButton/defaults.d.ts
Пропы GrButton, которые можно задать глобально через
<GrConfigProvider :component-defaults="{ GrButton: { … } }">.
Набор закрытый и осознанный: только оформление. modelValue, обработчики и
всё, что относится к конкретному экземпляру, через конфиг настраиваться не
должно — иначе получим глобальное состояние, которое ищется часами.
interface GrButtonConfigurableProps {
variant: GrButtonVariant;
tone: GrButtonTone;
size: GrButtonSize;
square: boolean;
loadingIndicator: GrButtonLoadingIndicator;
prefixCell: boolean;
}Файлdist/types/components/GrButton/grButtonStyles.d.ts
type GrButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'ghost-border';Вид загрузки: спиннер, свет по подписи или отрезок, бегущий по нижней кромке.
type GrButtonLoadingIndicator = 'spinner' | 'shimmer' | 'track';type GrButtonTone = GrTone;type GrButtonSize = GrComponentSize;Файлdist/types/components/GrButton/safelist.d.ts
const grButtonSafelist: string[];Файлdist/types/components/GrButton/GrButton.vue.d.ts
interface GrButtonProps {
/**
* Вес кнопки: `primary` — заливка тона, `secondary` — мягкая подложка с
* рамкой, `outline` — только рамка, `ghost` — только текст, `ghost-border` —
* текст, рамка которого проявляется под указателем. По умолчанию `primary`;
* глобально — `componentDefaults`.
*/
variant?: GrButtonVariant;
/**
* Смысл действия: цвет заливки, рамки и текста. `danger` — для необратимого,
* `neutral` — для второстепенного. По умолчанию `primary`; внутри
* `GrButtonGroup` — тон группы.
*/
tone?: GrButtonTone;
/** Высота и кегль по шкале контролов. По умолчанию `md`; берётся из группы и `GrConfigProvider`. */
size?: GrButtonSize;
/**
* Действие выполняется: спиннер на месте префикса, а без него — поверх
* подписи, ширина не меняется; `aria-busy` и перехват клика. Кнопка при этом
* остаётся в фокусе — нативный `disabled` не ставится.
*/
loading?: boolean;
/**
* Вид загрузки. `spinner` — спиннер; `shimmer` — свет проходит по подписи, а
* подпись, иконки и ширина остаются на месте. Без текстовой подписи (кнопка из
* иконки) мерцать нечему — там спиннер. `track` — отрезок бежит по нижней
* кромке, содержимое на месте. По умолчанию `spinner`; тема меняет его через
* `componentDefaults` («Глина» — `shimmer`, «Чертёж» — `track`).
*/
loadingIndicator?: GrButtonLoadingIndicator;
/**
* Префикс в квадратной ячейке высотой с кнопку, за перегородкой. Только с
* подписью: у квадратной кнопки ячейка и есть кнопка. По умолчанию `false`;
* тема включает её через `componentDefaults` («Чертёж»).
*/
prefixCell?: boolean;
/** i18n: что именно грузится. `aria-busy` сам по себе часть AT не объявляет. */
loadingText?: string;
/**
* Действие недоступно: нативный `disabled` у `<button>`, у кнопки-ссылки —
* `aria-disabled` и снятый адрес. Цвета — из токенов, без прозрачности.
*/
disabled?: boolean;
/** Равные стороны — кнопка из одной иконки. Без текста ей нужен `ariaLabel`. */
square?: boolean;
/** Кнопка на всю ширину контейнера. */
block?: boolean;
/** Нативный `type` у `<button>`. По умолчанию `button`: кнопка в форме не отправляет её сама. */
type?: 'button' | 'submit' | 'reset';
/** Доступное имя кнопки, когда текста нет: `square` с одной иконкой без него безымянна. */
ariaLabel?: string;
/** Полиморфизм: кастомный корневой тег/компонент (например, RouterLink). */
as?: string | Component;
/** Рендерит кнопку как `<a href>` (если не задан `as`). */
href?: string;
/** `target` кнопки-ссылки. С `_blank` сам добавляется `rel="noopener noreferrer"`. */
target?: string;
/** `rel` кнопки-ссылки. Без него при `target="_blank"` ставится `noopener noreferrer`. */
rel?: string;
/**
* Открывать ссылку в новой вкладке: `target="_blank"` и `rel="noopener noreferrer"`.
* Уход объявляется заранее — значком справа и скрытым текстом в имени.
*/
external?: boolean;
}Файлdist/types/components/GrButton/index.d.ts
type GrButtonInstance = ComponentExposed<typeof GrButtonComponent>;Использование из прозы библиотеки
disabled и loading — разные состояния
disabled ставит нативный атрибут (у <button>) и гасит кнопку токенами
--gr-button-disabled-bg / -fg / -brd. Прозрачности здесь нет намеренно:
opacity разбавляет выверенные на AA цвета, а кнопка чаще других компонентов
стоит на цветной подложке, где разбавленный текст проваливается первым.
Сами эти токены — ссылки на общие роли --gr-disabled-bg/-fg/-brd, те же, что у
вкладки, сегмента и бегунка: недоступное состояние во всём пакете выглядит
одинаково и перекрашивается одним местом. Покомпонентная тройка остаётся точкой
кастомизации на случай, когда кнопка обязана отличаться.
Приглушение работает и для кнопки-ссылки (href/as): нативный disabled ей
не достаётся, поэтому цвета задаются классами, а не disabled:-вариантом.
Кнопку, которая должна остаться в таб-порядке, но объявлена недоступной,
помечают атрибутом aria-disabled="true". Она получает тот же приглушённый вид,
нативный disabled не ставится, а клик компонент не перехватывает — его гасит
потребитель. Так GrTransfer держит перенос, которому некуда ехать: уронить
фокус с нажатой кнопки в тело документа было бы хуже.
loading не ставит нативный disabled — элемент остался бы без фокуса, и
скринридер потерял бы контекст. Вместо этого aria-busy, aria-disabled и
перехват клика в capture-фазе. Выглядит кнопка при этом обычной: о состоянии
говорит спиннер. Курсор — progress, а не not-allowed: кнопка занята, а не
запрещена. Вдавливания на нажатие нет ни у ждущей, ни у отключённой — они
нажатие не принимают и не изображают.
Загрузка не меняет ширину
С префиксом спиннер встаёт на его место. Без префикса ему некуда встать, и он ложится поверх содержимого, а содержимое гаснет: соседние кнопки в ряду не едут, как ехали бы от дописанного слева спиннера.
Гаснет оно прозрачностью, а не visibility — иначе подпись выпала бы из
доступного имени. Голый текстовый узел селектором не достать, поэтому текст
гасится -webkit-text-fill-color: color остаётся прежним, и спиннер рисуется
цветом подписи.
<GrButton :loading="saving" loading-text="Сохраняем отчёт">
Сохранить
</GrButton>aria-busy сам по себе объявляет не всякая AT, поэтому во время загрузки к
имени кнопки добавляется скрытый суффикс — loadingText или ключ
gr.button.loading.
Свет по подписи вместо спиннера
loadingIndicator="shimmer" — другой сигнал той же загрузки: спиннера нет, свет
проходит по глифам подписи, а подпись, иконки аддонов и ширина остаются на месте.
aria-busy, скрытый суффикс и перехват клика — те же, что у спиннера.
<GrButton tone="neutral" loading-indicator="shimmer" :loading="saving">
Сохранить черновик
</GrButton>- Градиент — на подписи, а не на кнопке.
background-clip: textна корне срезал бы его заливку, поэтому свет кладётся на обёртку текста;colorне трогается, и иконки, нарисованные им, не мерцают. - Только у текстовой подписи. У кнопки из одной иконки мерцать нечему — там остаётся спиннер, иначе загрузку не было бы видно.
prefers-reduced-motion. Мерцания нет, подпись приглушена — другого сигнала, кроме неё, у этого вида загрузки нет.
Вид загрузки — оформление, поэтому его задаёт и тема (componentDefaults в
theme.json: так он включён у «Глины»), и GrConfigProvider. Длительность прохода
— токен темы --gr-button-shimmer-duration.
Отрезок по кромке вместо спиннера
loadingIndicator="track" — загрузка отрезком, бегущим по нижней кромке поверх рамки:
подпись, иконки и ширина на месте, подпись чуть приглушена. Кромка есть у любой
кнопки, поэтому квадратная тоже бежит отрезком, а не спиннером. aria-busy, скрытый
суффикс и перехват клика — те же.
С prefers-reduced-motion отрезок не выключается, а замедляется: другого сигнала у
загрузки нет. Период — токен темы --gr-button-track-duration; вид включает тема
(«Чертёж»).
Ячейка иконки
prefixCell ставит префикс в квадратную ячейку высотой с кнопку, за перегородкой в
24 % цвета подписи, и снимает боковой отступ с её стороны. Ячейка — только у кнопки с
подписью: у квадратной кнопки ячейка и есть кнопка. Спиннер загрузки занимает ячейку,
а не встаёт рядом. Включает её тема через componentDefaults («Чертёж»).
Слоты
<GrButton>
<template #prefix><GrIcon><IconPlus /></GrIcon></template>
Добавить
<template #suffix><GrKbd keys="mod+N" /></template>
</GrButton>Во время loading спиннер занимает место префикса, а #prefix не рендерится:
две иконки рядом читаются как ошибка вёрстки. У мерцания и отрезка префикс остаётся
на месте — спиннера там нет.
Со стороны непустого аддона боковой отступ на 2px меньше — глиф стоит оптически
по центру, а не прижатым к краю. Пустой слот (v-if внутри шаблона) отступ не
трогает.
Иконка в слоте берёт ступень от кнопки: xs → 14px, sm и md → 16px,
lg → 20px. Кнопка подсказывает её GrIcon контекстом; явный size у иконки
сильнее подсказки, подсказка сильнее GrConfigProvider. Зазор между частями
растёт так же: 4 / 6 / 8 / 8px.
Длинная подпись режется
Подпись из одного текста кнопка оборачивает и режет многоточием, а полный текст показывает нативной подсказкой — только когда строка правда обрезана. Доступное имя остаётся целым. Иконку или разметку в основном слоте кнопка не оборачивает: обёртка сдвинула бы их выравнивание, и квадратная кнопка перестала бы быть квадратом.
Фокус — контур с отступом
Фокус рисуется outline в 2px цветом --gr-ring, только на :focus-visible;
отступ — токен темы --gr-button-focus-offset (у «Сланца» 2px). Контур может
въезжать издалека: --gr-button-focus-offset-from — отступ в момент появления («Тактильная» —
6px). Кольцо вплотную к заливке того же тона читалось утолщением
кнопки — индиго на индиго даёт 1.4:1, — а box-shadow в forced-colors
выключается целиком. outline там остаётся и перекрашивается в системный цвет.
Веса не спорят с главным
У «Сланца» рамка secondary в покое — 38 % тона, outline — 70 %; полный тон рамка
получает под указателем. Насыщенная рамка у второстепенного веса тянула взгляд мимо
primary. Нейтральная secondary — «обычная» кнопка формы: поверхность карточки
(--gr-button-neutral-soft-bg), рамка --gr-button-neutral-border и тень
--gr-button-surface-shadow.
У solid-кнопки есть объём: тень темы (--gr-button-shadow), под ней — тень цвета тона
(--gr-button-glow с цветом --gr-button-<тон>-glow) и слой поверх заливки
(--gr-button-solid-image); в нажатии тень уходит внутрь (--gr-button-shadow-active).
У «Сланца» это блик по верхней кромке и мелкая тень, тени тона и слоя нет. Отключённая
кнопка объём теряет вместе с вариантом. Рамка в 1px есть у всех весов — у ghost
прозрачная, — поэтому ghost в ряду той же ширины, что соседи.
Подпись — токен темы --gr-button-font-weight: у «Сланца» medium, у «Чертежа» 600.
Наведение и нажатие soft-заливки подмешивают тон в саму заливку, а не в фон. Поэтому они темнеют в светлом режиме и светлеют в тёмной — вслед за покоем.
Вид — у темы, механика — у кнопки
Форма, отклик и объём — токены вида theme: тема задаёт их значения, кнопка их только
читает. Отсюда четыре разных кнопки на одном API — «Сланец», «Тактильная», «Чертёж» и
«Глина»; разбор — ../design/GrButton.md.
- Скругление — по ступени:
--gr-button-radius-{xs,sm,md,lg}. Один радиус на все ступени — ручка приложения--gr-button-radius(999px— капсула); тема её не задаёт. - Отклик под указателем — слой
::beforeпод содержимым:::afterзанят зоной касания. Его положение, размер, цвет и рост задают--gr-button-response-*; цвет считается отcurrentColor, поэтому один токен годится всем тонам. У «Тактильной» это подложка прозрачных весов, у «Чертежа» — линия хода по нижней кромке, у «Сланца» слой прозрачен. - Подъём и темп —
--gr-button-lift,--gr-button-motion-durationи-ease; сprefers-reduced-motionдвижения нет, остаётся смена цвета. - Недоступная кнопка берёт стиль рамки и слой поверх заливки у темы
(
--gr-button-disabled-border-style,-image): пунктир и штриховка читаются без цвета.
Нажатый переключатель держит вид нажатия
Кнопка с aria-pressed="true" — включённый переключатель — держит вид нажатого
состояния своего веса (атрибут data-gr-pressed): иначе «включено» и «выключено»
различались бы только для диктора. В лотке GrButtonGroup выбранное звено — лист
поверх лотка, атрибута нет. aria-current вид не меняет: у ссылки это навигация, и
её вид решает раскладка, а не кнопка.
Зона касания на тач-экране
На грубом указателе (pointer: coarse) кнопка ниже --gr-button-touch-size
(44px) добирает высоту невидимым ::after. Вид и плотность ряда прежние: xs в
строке таблицы остаётся 28px, а промахов пальцем нет.
Квадратная кнопка и block
square даёт кнопку с равными сторонами — размер приходит из
--gr-button-square-size с дефолтом по размеру (1.75rem / 2rem / 2.5rem /
2.75rem). Переменную можно задать в своём CSS и подогнать кнопку под свою
сетку:
.toolbar { --gr-button-square-size: 2.25rem; }Переменная одна на все четыре ступени. Заданная выше по дереву — на :root,
в теме — она схлопывает xs, sm, md и lg в одно число, и ступень
перестаёт что-либо значить. Поэтому её место — узкий скоуп вроде примера выше, а
не корень документа. Нужен размер по устройству ввода — задавайте его
GrConfigProvider, а не переменной: приложение знает про свой указатель, пакет
про него не знает.
block растягивает кнопку на ширину контейнера.
Полиморфный корень
as → <a href> → <button>. Ссылке автоматически достаётся
rel="noopener noreferrer", если она открывается в новой вкладке (external
или явный target="_blank"), а disabled убирает её из таб-порядка.
Уход в новую вкладку объявляется заранее, как у GrLink (WCAG 3.2.5): справа
появляется значок, а к имени — скрытое «(откроется в новой вкладке)», ключ
gr.button.opensInNewTab. При заданном ariaLabel подсказка дописывается в само
имя, иначе aria-label перекрыл бы её. У квадратной кнопки значка нет — места
под него нет, — но подсказка остаётся.
Пропы этого примера