Кнопки и ссылки

Девять вариантов кнопки, три размера, группа, квадратная под иконку — и ссылка, которую ресет иначе оставил бы неотличимой от текста.

griffincss-ui packages/ui/scss/_button.scss packages/ui/scss/_link.scss

Класс ставится на любой элемент — <button>, <a>, <input type="submit">. Оформление одинаковое, семантику выбирает разметка: то, что отправляет форму, — кнопка; то, что ведёт на другой адрес, — ссылка.

Варианты

КлассВидКогда
.gr-btnПоверхность с границейОбычное действие, каких на экране много
.gr-btn-primaryЗаливка акцентомГлавное действие экрана — одно на форму
.gr-btn-secondaryКонтур акцентомДействие рядом с главным: «Отмена», «Назад»
.gr-btn-successЗаливка зелёнымПодтверждение, публикация
.gr-btn-warningЗаливка янтарнымДействие с последствиями, но обратимое
.gr-btn-dangerЗаливка краснымНеобратимое: удаление, отзыв доступа
.gr-btn-infoЗаливка пурпурнымСправка, подробности
.gr-btn-ghostБез фона и границыПанель инструментов, плотный ряд действий
.gr-btn-linkВыглядит ссылкойТретьестепенное действие внутри текста
Варианты
<button class="gr-btn gr-btn-primary">Сохранить</button>
<button class="gr-btn gr-btn-secondary">Отмена</button>

Цвет текста на заливке — один токен на три статуса. Заливка берёт --gr-color-success и подобные, а текст — --gr-color-on-accent. В светлой теме статусные цвета тёмные, а on-accent белый; в тёмной они светлеют, а on-accent становится почти чёрным. Пара остаётся читаемой в обеих темах, и второго набора правил под тёмную не требуется.

Предупреждение из этого правила выпадает. Чтобы --gr-color-warning читался текстом на белом, он обязан быть тёмно-коричневым — а прямоугольник такого цвета читается не как «внимание», а как «оливковый». Поэтому у статуса есть своя пара --gr-color-warning-surface и --gr-color-on-warning: в обеих темах кнопка остаётся янтарной с тёмным текстом. Зелёный, красный и пурпурный узнаваемы и в тёмном варианте, своей пары им не нужно.

Наведение и нажатие

Вариант задаёт цвета четырьмя переменными — --gr-btn-bg, --gr-btn-fg, --gr-btn-border и триплетом --gr-btn-fg-hsl. Состояния :hover и :active написаны один раз на все девять: они кладут поверх фона варианта вуаль из его же цвета текста. Насколько это темнее или светлее, решает действующая тема, а не таблица заготовленных оттенков.

--gr-btn-veil-hover: hsl(var(--gr-btn-fg-hsl), 12%);
--gr-btn-veil-active: hsl(var(--gr-btn-fg-hsl), 24%);

/* вуаль ложится внутренней тенью — поверх фона, но под текстом */
box-shadow: inset 0 0 0 100vmax var(--gr-btn-veil-hover);

Вуаль в 12 % даёт ровно то же, что дало бы смешение 88 % фона с 12 % текста, и не требует color-mix(): альфу приписывают к записи hsl(), поэтому цвет текста варианта продублирован триплетом. Свой цвет вуали задают, переопределив --gr-btn-veil-hover и --gr-btn-veil-active; значение transparent снимает подсветку вовсе — так сделано у .gr-btn-link.

Подсветка живёт внутри @media (hover: hover). На сенсорном экране :hover залипает после касания — кнопка остаётся подсвеченной до тех пор, пока не тронут другую.

Размеры

КлассВысотаТокен
.gr-btn-sm2rem--gr-control-height-sm
.gr-btn2.5rem--gr-control-height
.gr-btn-lg3rem--gr-control-height-lg
Размеры

Высоты заданы в rem, а не в пикселях. Режим для слабовидящих поднимает кегль корня документа — и кнопки растут вместе со страницей, без единого дополнительного правила. Включите переключатель «Для слабовидящих» на боковой панели и сравните.

Форма

КлассПоведение
.gr-btn-blockВо всю ширину родителя; display меняется на flex, иначе кнопка оставила бы за собой строку
.gr-btn-iconКвадратная: ширина берётся из той же переменной, что и высота, поэтому .gr-btn-icon.gr-btn-lg остаётся квадратом
.gr-btn-groupСлипшийся ряд: скругления остаются только на краях, соседние границы не удваиваются
Иконка и группа

У квадратной кнопки внутри только знак — подписи нет, значит нет и имени для скринридера. aria-label обязателен: без него кнопка озвучивается как «кнопка» и ничего больше.

Состояния

СостояниеКак задаётсяЧто происходит
Недоступнаdisabled или aria-disabled="true"Приглушается, курсор not-allowed. Атрибута disabled у <a> не существует — там нужен aria-disabled
Занятаaria-busy="true"Приглушается, курсор progress
Фокус с клавиатуры:focus-visibleКонтур толщиной --gr-focus-width цветом --gr-color-focus
Состояния (пройдите по ним табом)
Ссылка-кнопка

Кольцо загрузки у aria-busy появится вместе с модулем _spinner — это волна 8c. Сейчас состояние видно по приглушению и курсору, и этого достаточно, чтобы кнопку не нажали дважды.

Ссылки

Компонент нужен ровно потому, что ресет снимает с <a> и цвет, и подчёркивание (color: inherit; text-decoration: none). Это сделано намеренно — ссылка-обёртка вокруг карточки не должна красить её содержимое, — но обычная ссылка в тексте без класса остаётся неотличимой от текста.

КлассВидКогда
.gr-linkЦвет ссылки, подчёркивание, три состоянияСсылка в тексте — случай по умолчанию
.gr-link-mutedЦвет вторичного текстаПодписи, футер, служебные переходы
.gr-link-inheritЦвет наследуется от окруженияСсылка-заголовок, ссылка на цветной плашке
.gr-link-quietПодчёркивание только при наведении и фокусеНавигация, ряд карточек — но не текст абзаца
.gr-link-externalСтрелка-указатель после текстаПереход на чужой сайт или в новую вкладку

Стиль линии

Линия несёт смысл, которого нет у цвета, и переживает чёрно-белую печать. Стиль сочетается с любым вариантом: .gr-link-quiet.gr-link-dotted покажет точки при наведении, .gr-link-muted.gr-link-dashed — пунктир приглушённым цветом.

КлассЛинияЧем принято помечать
СплошнаяОбычный переход
.gr-link-dottedТочкамиТермин со справкой, сноска — то, что откроется рядом
.gr-link-dashedПунктиромТо же, но заметнее
.gr-link-wavyВолнойПроблемное: устаревший адрес, неточный перевод
.gr-link-doubleДвойнаяГлавная ссылка абзаца
Стили линии

Линия объявлена только длинными свойствамиtext-decoration-line, -thickness, -style. Сокращённое text-decoration сбрасывает все остальные длинные в исходные значения, поэтому одно text-decoration: underline в правиле :hover обнуляло бы толщину from-font из базового класса — и линия у тихой ссылки переставала совпадать с линией у обычной. Толщина from-font берётся у шрифта, поэтому остаётся соразмерной кеглю и в подписи, и в заголовке. Единственное место, где она уступает, — режим для слабовидящих: там линию рисует правило темы, и она нарочно толще.

Ссылки

Обычная ссылка внутри абзаца — цвет и подчёркивание.

Приглушённая: условия использования.

Наследующая цвет: заголовок-ссылка.

Тихая (наведите или пройдите табом): раздел навигации.

Внешняя: репозиторий (откроется в новой вкладке).

<a class="gr-link" href="/страница">обычная ссылка</a>

<a class="gr-link gr-link-external" href="https://пример" target="_blank" rel="noopener">
  внешняя<span class="gr-sr-only"> (откроется в новой вкладке)</span>
</a>

Стрелка — признак только для глаза. Она нарисована псевдоэлементом, а его нельзя пометить aria-hidden, и скринридер прочитает её как «стрелка на северо-восток». О переходе на чужой сайт или об открытии новой вкладки сообщает текст в .gr-sr-only рядом — как в примере выше.

У .gr-link-quiet есть граница применимости. Снимать подчёркивание можно там, где ссылка опознаётся по положению: список навигации, ряд карточек, футер. В тексте абзаца положение не говорит ни о чём, и ссылка остаётся отличима только цветом — то есть не отличима вовсе для дальтоника и на чёрно-белой печати.

Режим для слабовидящих перекрывает оба «тихих» варианта: правило [data-gr-a11y="low-vision"] a[href] из темы возвращает и цвет ссылки, и подчёркивание. Оно выигрывает по специфичности, потому что тема приезжает в каждую сборку и попадает в тот же слой, что и компоненты. Включите переключатель на боковой панели и посмотрите на тихую ссылку выше.

Ссылка и кнопка-ссылка — разные вещи

.gr-link.gr-btn-link
Элемент<a href><button>
Что делаетПереход по адресуДействие на текущей странице
КлавишаEnterEnter и Space
ВысотаПо строке текста--gr-control-height: выравнивается в ряду кнопок

Выбирает разметка, оформление лишь подтверждает выбор. Кнопка, ведущая на другой адрес, ломает Ctrl+клик, среднюю кнопку мыши и контекстное меню «Открыть в новой вкладке»; ссылка, выполняющая действие, отправляет в историю браузера запись, по которой некуда вернуться.

Отступы — утилитами

Своих внешних полей у кнопки нет: расстановка — дело раскладки. Слой griffincss.utils старше griffincss.ui, поэтому любая утилита перекрывает компонент без !important и без гонки специфичностей.

<div class="gr-flex gr-gap">
  <button class="gr-btn gr-btn-primary gr-flex-1">Сохранить</button>
  <button class="gr-btn">Отмена</button>
</div>

Слот под иконку

Своего набора иконок Griffincss не везёт и не будет: набор — отдельный продукт со своим темпом обновления и своей лицензией. Вместо набора библиотека даёт слот и обещание, что иконка в нём встанет правильно с любым набором. Отдельного класса .gr-icon для этого не нужно — и это не экономия, а проверенный результат: всё, что слот должен делать, уже делают сами компоненты.

Что обещает библиотека

ОбещаниеЧем обеспечено
Иконку не нужно выравнивать.gr-btn, .gr-badge, .gr-menu-item, .gr-nav-link — это flex с align-items: center и своим gap
Зазор между иконкой и текстом заданgap: var(--gr-ui-gap); у плашки — половина от него
Иконка следует цвету текстацвет наследуется; иконка обязана объявить fill="currentColor" или stroke="currentColor"
Иконка следует кеглюдля глифа и иконочного шрифта — сама собой, это текст
Готовая коробка 1,25 em.gr-alert-icon, .gr-toast-icon, .gr-menu-iconflex: none и фиксированный квадрат
Квадратная кнопка под одну иконку.gr-btn-icon; требует aria-label

Чего библиотека ждёт от иконки

ТребованиеПочему
Инлайновый <svg> объявляет свой размер самSVG без размеров берёт свои 300×150 px; утилиты размера в em в библиотеке нет, а .gr-w-[1em] — произвольное значение и требует подключённого griffincss.js
Декоративная иконка скрыта от скринридераaria-hidden="true": иначе читалка озвучит её вместе с текстом кнопки
Иконка без текста несёт aria-labelу кнопки-иконки другого имени нет
Иконка — не единственный признакто же правило, что у цвета: значок без подписи угадывается, а не читается

Выравнивание по базовой линии внутри компонента утилитой не задаётся. .gr-flex-items-baseline живёт в слое griffincss.core, а align-items: center компонента — в слое griffincss.ui, который идёт позже и потому выигрывает. Ловушка та же, что у видимости: побеждает не специфичность, а порядок слоёв. Практического неудобства тут нет — в кнопке, плашке и пункте меню нужно ровно центрирование, — но знать об этом стоит до того, как класс будет добавлен и молча не сработает.

Два набора, ни одного глифа в репозитории

Примеры ниже показывают два распространённых способа подключить чужой набор. Оба — без зависимости от Griffincss и без единого значка в его составе: библиотека описывает слот, а не заполняет его.

Иконочный шрифт. Значок — символ, поэтому кегль и цвет он берёт сам, а от разметки ему нужно только имя класса набора и aria-hidden.

<button class="gr-btn gr-btn-primary">
  <i class="bi bi-download" aria-hidden="true"></i>
  Скачать
</button>

<button class="gr-btn gr-btn-icon" aria-label="Настройки">
  <i class="bi bi-gear" aria-hidden="true"></i>
</button>

Спрайт из <svg>. Размер объявляется атрибутами, цвет — currentColor. Атрибуты, а не класс: без них SVG развернётся во все свои 300×150 px, и произойдёт это ещё до того, как выполнится любой скрипт.

<button class="gr-btn">
  <svg width="16" height="16" fill="currentColor" aria-hidden="true">
    <use href="icons.svg#download"></use>
  </svg>
  Скачать
</button>
Слот работает и на символе: кегль и цвет наследуются, зазор даёт gap кнопки

Три размера кнопки в этом ряду — проверка обещания «иконка следует кеглю»: ни на одной из них у значка нет своего класса размера.

Примеры использования

Кнопка почти всегда стоит в компании: пара CTA в hero, ряд действий формы, слитая группа-переключатель. Рецепты ниже — три самые частые компании и роли в них; состояние загрузки разобрано на странице Transitions.

Пара CTA: главная и вторая

Первый экран продаёт одно действие — залитая .gr-btn-primary, а второй даёт запасной выход — тихая .gr-btn-ghost со стрелкой. Иерархия строится контрастом заливки, а не размером: обе кнопки .gr-btn-lg и стоят в одном флекс-ряду. На мобильном ряд складывается и кнопки растягиваются: .gr-flex-col .gr-flex-row-sm на контейнере, .gr-w-full .gr-w-auto-sm на кнопках.

<div class="gr-flex gr-flex-col gr-flex-row-sm gr-gap-sm gr-flex-center">
  <button class="gr-btn gr-btn-primary gr-btn-lg gr-w-full gr-w-auto-sm">Попробовать бесплатно</button>
  <button class="gr-btn gr-btn-ghost gr-btn-lg gr-w-full gr-w-auto-sm">Как это работает →</button>
</div>
Иерархию задаёт заливка; на узком экране — столбик во всю ширину

Кнопка с иконкой и ряд действий

Иконка уточняет глагол: «⬇ Скачать», «+ Добавить». Кнопка уже выравнивает содержимое по центру — иконке хватает быть первым символом или span-ом внутри. В ряду действий формы у каждой кнопки своя роль: опасное действие уводится в начало строки и в .gr-btn-ghost .gr-text-danger, пара «Отмена / Сохранить» дожимается к концу строки через .gr-ms-auto — приём авто-маржина из Spacing.

<div class="gr-flex gr-flex-wrap gr-gap-sm gr-flex-items-center">
  <button class="gr-btn gr-btn-ghost gr-text-danger">Удалить черновик</button>
  <button class="gr-btn gr-ms-auto">Отмена</button>
  <button class="gr-btn gr-btn-primary">⬇ Сохранить</button>
</div>
Роли в ряду: опасное — в начале строки и тихим, основное — в конце и залитым

Группа-переключатель вида

«Список / сетка», «День / Неделя / Месяц» — слитая группа .gr-btn-group: скругления только на внешних углах, границы не удваиваются. Активный сегмент отмечается и стилем (.gr-btn-primary), и семантикой — aria-pressed="true": скринридеру заливка не видна. Капсульный вариант той же задачи — сегмент-контрол — разобран на странице Border Radius.

<div class="gr-btn-group" role="group" aria-label="Вид списка">
  <button class="gr-btn gr-btn-primary" aria-pressed="true">Список</button>
  <button class="gr-btn" aria-pressed="false">Сетка</button>
  <button class="gr-btn" aria-pressed="false">Канбан</button>
</div>
Слитая группа: активный сегмент залит и помечен aria-pressed