GrButton

  • ядро
  • Действия
  • API · 15 props

Берут, когда действие меняет состояние.

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

Обзор

Когда брать

  • действие меняет состояние — сохранить, удалить, отправить: адрес при этом не меняется;
  • действие идёт на сервер — loading блокирует повтор и объявляет ожидание;
  • важность действия видна — variant и tone разводят главное, второстепенное и опасное;
  • кнопка только из иконки — square вместе с ariaLabel: без имени такая кнопка безымянна;
  • нужна ссылка с видом кнопки — href/as подменяют тег, оставляя семантику ссылки.

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

НужноБерите
Переход меняет адресGrLink
Действий несколько в рядGrButtonGroup
Действий много, они прячутсяGrDropdownMenu
Переключается состояние «включено/выключено»GrSwitch
Переключается вид одного содержимогоGrSegmented

Варианты все тона и виды — настоящим компонентом

Варианты · GrButton
tone \ variantprimarysecondaryoutlineghostghost-border
primary
neutral
success
warning
danger
info
slate
azure

Размеры

  • xs
  • sm
  • md
  • lg

Состояния

  • обычная
  • disabled
  • loading

Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.

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

Сцена

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

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

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

  • Панель с одним громким действием

    Второстепенные инструменты — variant="ghost-border": тихие в ряду, с рамкой под указателем. Слоты prefix и suffix держат иконку, число фильтров и сочетание клавиш, и подпись с ними не сталкивается. Квадратная (square) кнопка из одной иконки получает имя из aria-label; залита только та, что создаёт.

    Invoices
    Northwind TradersINV-2026-0418 · Overdue€5,166.50
    Contoso LtdINV-2026-0417 · Paid€820.00
    Tailspin ToysINV-2026-0416 · Sent€310.00
  • Сохранить и отменить в подвале формы

    type="submit" и type="reset" отдают работу форме: Enter сохраняет, а сброс перехватывается и возвращает сохранённое. Пока идёт запрос, loading оставляет кнопку в фокусе и не пускает второй клик, а loading-text говорит скринридеру, что сохраняется. Обе кнопки ждут изменений — пока сохранять и отменять нечего.

    Billing details
    Unsaved changes
  • Разрушительное действие — через осознанный шаг

    tone="danger" помечает то, что не отменить, а кнопка с контуром только открывает шаг. Последняя кнопка остаётся disabled, пока не набрано имя, занимает всю ширину через block и показывает loading, пока пространство удаляется. Отмена возвращает клавиатуру на кнопку-триггер через открытый метод focus().

    Danger zone
    Transfer ownershipHand the workspace to another admin.
    Delete workspaceInvoices, customers and 12 members lose access.

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

Props

PropTypeпо умолчаниюОписание
disabledboolean | undefinedfalseДействие недоступно: нативный `disabled` у `<button>`, у кнопки-ссылки — `aria-disabled` и снятый адрес. Цвета — из токенов, без прозрачности.
type"button" | "submit" | "reset" | undefined"button"Нативный `type` у `<button>`. По умолчанию `button`: кнопка в форме не отправляет её сама.
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedСмысл действия: цвет заливки, рамки и текста. `danger` — для необратимого, `neutral` — для второстепенного. По умолчанию `primary`; внутри `GrButtonGroup` — тон группы.
variantGrButtonVariant | undefinedundefinedВес кнопки: `primary` — заливка тона, `secondary` — мягкая подложка с рамкой, `outline` — только рамка, `ghost` — только текст, `ghost-border` — текст, рамка которого проявляется под указателем. По умолчанию `primary`; глобально — `componentDefaults`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedВысота и кегль по шкале контролов. По умолчанию `md`; берётся из группы и `GrConfigProvider`.
ariaLabelstring | undefinedundefinedДоступное имя кнопки, когда текста нет: `square` с одной иконкой без него безымянна.
loadingboolean | undefinedfalseДействие выполняется: спиннер на месте префикса, `aria-busy` и перехват клика. Кнопка при этом остаётся в фокусе — нативный `disabled` не ставится.
loadingTextstring | undefinedundefinedi18n: что именно грузится. `aria-busy` сам по себе часть AT не объявляет.
squareboolean | undefinedundefinedРавные стороны — кнопка из одной иконки. Без текста ей нужен `ariaLabel`.
asstring | Component | undefinedundefinedПолиморфизм: кастомный корневой тег/компонент (например, RouterLink).
blockboolean | undefinedfalseКнопка на всю ширину контейнера.
hrefstring | undefinedundefinedРендерит кнопку как `<a href>` (если не задан `as`).
targetstring | undefinedundefined`target` кнопки-ссылки. С `_blank` сам добавляется `rel="noopener noreferrer"`.
relstring | undefinedundefined`rel` кнопки-ссылки. Без него при `target="_blank"` ставится `noopener noreferrer`.

Слоты

SlotTypeОписание
defaultanyСодержимое кнопки.
prefixanyАддон слева: иконка, счётчик. В состоянии загрузки уступает место спиннеру.
suffixanyАддон справа: шеврон, счётчик.

Методы

MethodsСигнатураОписание
focus() => voidПоставить фокус на кнопку — и когда корень тег, и когда компонент из `as`.
blur() => voidСнять фокус с кнопки.

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrButtonConfigurableProps, GrButtonVariant, GrButtonTone } from '@feugene/granularity/components/GrButton'

Файлdist/types/components/GrButton/config.d.ts

grButtonConfigconst
const grButtonConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrButton">;

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

GrButtonConfigurablePropsinterface

Пропы GrButton, которые можно задать глобально через <GrConfigProvider :component-defaults="{ GrButton: { … } }">. Набор закрытый и осознанный: только оформление. modelValue, обработчики и всё, что относится к конкретному экземпляру, через конфиг настраиваться не должно — иначе получим глобальное состояние, которое ищется часами.

interface GrButtonConfigurableProps {
    variant: GrButtonVariant;
    tone: GrButtonTone;
    size: GrButtonSize;
    square: boolean;
}

Файлdist/types/components/GrButton/grButtonStyles.d.ts

GrButtonVarianttype
type GrButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'ghost-border';
GrButtonTonetype
type GrButtonTone = GrTone;
GrButtonSizetype
type GrButtonSize = GrComponentSize;

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

grButtonSafelistconst

Только матрица тон × вариант: grButtonStyles.ts: variantClass клеит каждый её класс из префикса и токена тона (bg-${withVar(tokens.solidBackground)}), и целиком их нет ни в одной строке кода. Тон и вариант приходят пропом, из GrButtonGroup или GrConfigProvider, поэтому объявлена вся матрица — 8 × 5. Собранное узнаётся по скобкам, которые подставляет withVar. Литеральные куски той же функции (bg-transparent, border, border-transparent), база, размеры, квадрат, block и отключённое состояние лежат строками целиком — granum извлекает их из чанков компонента сам. Ту же матрицу рисует вид button у GrRadio (через grButtonClass): к нему она приезжает зависимостью GrButton вместе со всеми классами её чанка.

const grButtonSafelist: string[];

Файлdist/types/components/GrButton/GrButton.vue.d.ts

GrButtonPropsinterface
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;
    /** 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

GrButtonInstancetype
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-фазе. Выглядит кнопка при этом обычной: о состоянии говорит спиннер.

vue
<GrButton :loading="saving" loading-text="Сохраняем отчёт">
Сохранить
</GrButton>

aria-busy сам по себе объявляет не всякая AT, поэтому во время загрузки к имени кнопки добавляется скрытый суффикс — loadingText или ключ gr.button.loading.

Слоты
vue
<GrButton>
  <template #prefix><GrIcon><IconPlus /></GrIcon></template>
  Добавить
  <template #suffix><GrKbd keys="mod+N" /></template>
</GrButton>

Раньше иконка и текст валились в один слот, и порядок держался дисциплиной потребителя. Во время loading спиннер занимает место префикса, а #prefix не рендерится: две иконки рядом читаются как ошибка вёрстки.

Квадратная кнопка и block

square даёт кнопку с равными сторонами — размер приходит из --gr-button-square-size с дефолтом по размеру (1.75rem / 2rem / 2.5rem / 2.75rem). Переменную можно задать в своём CSS и подогнать кнопку под свою сетку:

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 убирает её из таб-порядка.