Справочник компонентов

Двадцать шесть модулей интерфейса на токенах ядра: кнопки, поля, карточки, таблицы, навигация, всплывающие слои, состояния.

griffincss-ui полный список

Готовые элементы интерфейса на токенах ядра. Обе темы и режим для слабовидящих приходят к ним даром — тем же механизмом, что и ко всему остальному: компоненты не знают ни одного литерального цвета и ни одной литеральной высоты.

Peer-зависимость — только griffincss-core. От griffincss-utils пакет не зависит: компонент обязан выглядеть правильно сам по себе, утилиты нужны, чтобы его подвинуть.

Именование плоское, в один дефис: .gr-btn-primary, а не .gr-btn--primary. Тот же строй, что в ядре и утилитах — двух синтаксисов в библиотеке нет.

Двадцать шесть модулей тремя волнами. Первая — статические: ни один не открывается, ни одному не нужен JS. Вторая — навигация и раскрытие: всё, что открывается, открывается средствами платформы — <details>, <dialog>, Popover API, — а не скриптом библиотеки. Третья — обратная связь и состояния: прогресс, кольцо занятости, заглушки на время загрузки, тосты, шаги процесса и пустое состояние.

Кнопки (scss/_button.scss)

КлассОписание
.gr-btnБазовая кнопка: поверхность с границей. Ставится на <button>, <a> или <input type="submit">
.gr-btn-primary / -secondaryЗаливка акцентом / контур акцентом
.gr-btn-success / -warning / -danger / -infoЗаливка цветом статуса
.gr-btn-ghost / -linkБез фона и границы / в виде ссылки
.gr-btn-sm / -lgРазмеры из --gr-control-height-sm / -lg
.gr-btn-blockВо всю ширину родителя
.gr-btn-iconКвадратная, под одну иконку; требует aria-label
.gr-btn-groupСлипшийся ряд: скругления только по краям, границы не удваиваются

Состояния: :hover (внутри @media (hover: hover)), :active, :focus-visible, :disabled, [aria-disabled="true"], [aria-busy="true"].

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

Ссылки (scss/_link.scss)

КлассОписание
.gr-linkЦвет ссылки, подчёркивание from-font, состояния :visited / :hover / :active / :focus-visible
.gr-link-mutedЦвет вторичного текста — подписи, футер
.gr-link-inheritЦвет наследуется от окружения — ссылка-заголовок, ссылка на плашке
.gr-link-quietПодчёркивание только при наведении и фокусе
.gr-link-externalСтрелка-указатель после текста
.gr-link-dotted / -dashed / -wavy / -doubleСтиль линии; сочетается с любым вариантом

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

.gr-link и .gr-btn-link — разные вещи: первая ведёт по адресу, вторая выполняет действие на текущей странице. Выбирает разметка, оформление лишь подтверждает выбор.

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

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

Формы (scss/_form.scss)

КлассОписание
.gr-fieldОбёртка «метка + поле + подсказка» одной колонкой
.gr-label / .gr-hint / .gr-errorМетка, пояснение, сообщение об ошибке
.gr-successПодпись о принятом значении: пара к .gr-error, тот же кегль и то же место
.gr-requiredЗвёздочка обязательности вручную — для групп, где обязательность лежит не на одном контроле. У поля с required она появляется сама
.gr-input / .gr-textarea / .gr-selectТри поля одной семьи: общая высота, кегль и состояния. У списка своя стрелка на currentcolor, отбитая от края на --gr-control-padding-x: положение системной задаёт браузер, и она прижимается вплотную к краю
.gr-input-sm / -lgРазмер — на любом из трёх полей
.gr-input-groupПоле с приклеенными аддонами
.gr-input-no-spinЧисловое поле без системных стрелок шага: в счётчике количества шагают кнопки рядом, и стрелки становятся вторым органом управления тем же числом. Клавиатурный шаг ↑/↓ остаётся
.gr-input-addonНеинтерактивная подпись у края поля
.gr-input на type="date"Нативный выбор даты в оформлении семьи; два вендорных правила снимают отступы внутреннего редактора, чтобы поле не было выше соседнего. Плата — системный календарь и формат по локали браузера: «Формы»

Состояния: :focus-visible, :user-invalid, [aria-invalid="true"], [aria-invalid="false"], :disabled, :read-only. :user-invalid, а не :invalid: пустое обязательное поле не должно краснеть до того, как его тронули.

Успех — только по явному [aria-invalid="false"]. Пары :user-valid у :user-invalid нет намеренно: браузер считает валидным любое непустое поле без ограничений, и форма зеленела бы сама собой по мере заполнения. Обе стороны ставит одна и та же серверная проверка.

Обязательность рисует .gr-field:has([required]) .gr-label::after — атрибут required уже стоит в разметке, и требовать рядом ещё и класс значило бы держать два источника правды.

Выбор (scss/_choice.scss)

КлассОписание
.gr-checkbox / .gr-radioСистемный элемент, цвет отметки — через accent-color
.gr-switchТумблер на <input type="checkbox">; дорожка и бегунок рисуются фоном
.gr-choice<label>-обёртка: подпись в строку с элементом и кликабельна целиком
.gr-segmentedСегментный переключатель из радиогруппы в <fieldset>
.gr-swatch / .gr-swatch-tagРадио в виде кружка цвета (--gr-swatch на элементе) или плашки размера, следом за .gr-radio внутри .gr-choice; выбранный — кольцо или заливка акцентом

Элемент не подменяется: класс ставится на сам <input>, а не на спрятанный input рядом с оформленным <span>. Родная разметка сохраняет участие в форме, клавиатуру и объявление состояния скринридером.

Ползунок (scss/_range.scss)

КлассОписание
.gr-range<input type="range">: фильтр цены, громкость, доля. Дорожка — оттенок currentcolor, бегунок — акцент
.gr-range-pairПара «от — до»: два ползунка в одной ячейке сетки, отрезок между ручками — наложение двух дорожек. Порядок значений держит виджет range слоя
--gr-range-track / --gr-range-thumbТолщина дорожки и размер бегунка. Обе в rem: режим для слабовидящих поднимает кегль корня, и ползунок растёт вместе с полем рядом
--gr-progressДоля пройденного пути одиночного ползунка, 0…1. Её пишет виджет range слоя GriffinJS; CSS переводит её в проценты (--gr-range-fill) и красит след
--gr-range-from / --gr-range-toГраницы отрезка в паре, 0…1, на самой .gr-range-pair: отрезок рисует она одна, и обе границы нужны ей сразу

Шесть вендорных псевдоэлементов, каждый — отдельным правилом: неизвестный селектор в группе делает невалидной всю группу, и бегунок остался бы системным в обоих движках. accent-color при этом не снимается — он единственное, что делает ползунок частью библиотеки там, где appearance: none ещё не применён.

Заполненную часть дорожки Firefox рисует сам (::-moz-range-progress) и без скрипта. У WebKit псевдоэлемента для неё нет: там след — градиент по --gr-progress, и без слоя он просто не появляется. Чисто-CSS-приём (тень бегунка, обрезанная дорожкой) отвергнут: та же обрезка съедает круглую ручку.

Направление следа задано ручкой --gr-range-dir, а не стороной в каждом градиенте: логической формы у градиента нет, а в RTL движок разворачивает ползунок — и одно правило :dir(rtl) разворачивает след во всех дорожках сразу, включая пару.

Карточки (scss/_card.scss)

КлассОписание
.gr-cardПоверхность с границей и скруглением; колонка на flex
.gr-card-header / -body / -footerШапка, тело, подвал
.gr-card-mediaКартинка во всю ширину; углы срезает сама карточка
.gr-card-raised / -flatТень вместо границы / только фон
.gr-card-interactiveКликабельна целиком, но в табе — один элемент со ссылкой

Токены компонента читаются с запасным значением, на компоненте не объявляются, поэтому переопределение на любой обёртке доходит до карточки:

ТокенПо умолчаниюЧитает
--gr-card-bg--gr-color-surface.gr-card, фон
--gr-card-border--gr-color-border.gr-card, цвет обводки
--gr-card-radius--gr-radius-container--gr-radius.gr-card, скругление
--gr-card-pad--gr-gap.gr-card-body целиком; -header и -footer — по горизонтали тот же, по вертикали ¾
--gr-card-overflowhidden.gr-card, обрезка содержимого: hidden срезает медиа по скруглению; visible — для панели, из которой выходит абсолютный список подсказки

Таблицы (scss/_table.scss)

КлассОписание
.gr-tableСхлопнутые границы, отступы в ячейках, приглушённая шапка
.gr-table-striped / -bordered / -compact / -relaxed / -hoverПолосы, полная сетка, плотный и разреженный варианты, подсветка строки
.gr-table-wrapОбёртка с горизонтальной прокруткой — вбок уезжает таблица, а не страница
.gr-table-stickyЛипкая шапка внутри прокручиваемой области; линию под ней рисует внутренняя тень — схлопнутая граница с уехавшей ячейкой не едет
.gr-table-stack, -{sm,md,lg,xl}, -c{sm,md,lg,xl}Карточки вместо строк: всегда, ниже ширины окна или ниже ширины ближайшего .gr-cq. Суффикс называет порог: -md — карточки, пока уже 768 px. Шапка ужата в пиксель (не display: none — читалке нужна), подпись ячейки — из data-label, без атрибута — без подписи
th[aria-sort]Указатель направления сортировки у заголовка — рисуется из aria-sort, а не из класса, и потому работает и на таблице, отсортированной сервером
[data-gr-sortable]Сортировка строк виджетом слоя GriffinJS: атрибут на <table>, aria-sort="none" на сортируемых заголовках, необязательный data-gr-sort-value на ячейке — «Таблицы»

Сообщения (scss/_alert.scss)

КлассОписание
.gr-alertСообщение о статусе
.gr-alert-info / -success / -warning / -dangerСтатус
.gr-alert-titleСлово статуса — обязательная часть
.gr-alert-icon / -bodyСлот под иконку и текст
.gr-alert-dismissibleОтступ под кнопку закрытия

Слово берёт --gr-alert-color (безопасный для текста), полоса и подложка — --gr-alert-fill. У предупреждения это разные цвета: подробности ниже, в разделе о токенах.

Статус читается словом, а не только цветом: цветная полоса не переживает чёрно-белую печать, дальтонизм и монохромный режим высокого контраста, а слово переживает. Роль (role="alert" или role="status") объявляется разметкой — модуль её не подставляет.

Плашки (scss/_badge.scss)

КлассОписание
.gr-badgeПлашка статуса; цвета -accent / -success / -warning / -danger / -info
.gr-badge-pill / -outline / -countПилюля, контур, счётчик
.gr-tag / .gr-tag-removeТег и кнопка-крестик; крестику нужен data-gr-dismiss и рантайм
.gr-dotТочка-индикатор; цвета те же

Статус задаёт три цвета, а не один: --gr-badge-color для текста и линии, --gr-badge-fill для заливки и --gr-badge-on-fill для чернил на ней. У четырёх статусов из пяти первые два совпадают; расходятся они у предупреждения — см. --gr-color-warning-surface в таблице токенов.

Аватары (scss/_avatar.scss)

КлассОписание
.gr-avatarКруг с инициалами; <img> внутри перекрывает их собой
.gr-avatar-xs / -sm / -lg / -xl1.5rem / 2rem / 3rem / 4rem
.gr-avatar-squareСкруглённый квадрат вместо круга
.gr-avatar-groupРяд с перекрытием

Фолбэк не требует скрипта: нет картинки — нет и перекрытия, инициалы остаются видны. Битая ссылка даёт тот же результат.

Навигация (scss/_nav.scss)

КлассОписание
.gr-navРяд ссылок; работает и на <nav>, и на <ul> — список обнуляется здесь же
.gr-nav-linkПункт: ссылка или кнопка
.gr-nav-verticalКолонка вместо ряда — боковое меню раздела
.gr-nav-underlineАктивный пункт подчёркнут, а не залит
.gr-navbar / .gr-navbar-brandШапка сайта и название в ней
.gr-nav-toggle / .gr-nav-collapseПереключатель на <details> и сворачиваемая навигация
ТокенПо умолчаниюЧитает
--gr-nav-item-pad0.gr-nav > li — отступ пункта; правило темы на самом <li> проиграло бы слою компонентов
--gr-nav-link-radius--gr-radius.gr-nav-link, скругление; в .gr-nav-underline радиус по-прежнему 0

Активный пункт помечается aria-current="page" — тем же признаком, по которому его находит программа чтения с экрана. Отдельного класса на это нет: класс подсветил бы пункт только зрячему.

Сворачивание не прячет навигацию внутрь <details>: содержимое <details> скрывает сам браузер, и вернуть его на широком экране без скрипта нечем. Навигация стоит следующим соседом переключателя, поэтому раскрытием управляет .gr-nav-toggle[open] + .gr-nav-collapse, а с 768px медиазапрос отменяет сворачивание целиком:

<div class="gr-navbar">
  <a class="gr-navbar-brand" href="/">Сайт</a>
  <details class="gr-nav-toggle"><summary>Меню</summary></details>
  <nav class="gr-nav gr-nav-collapse">…</nav>
</div>

Хлебные крошки (scss/_breadcrumb.scss)

КлассОписание
.gr-breadcrumbПуть до страницы; разметка — <nav> с <ol>
.gr-breadcrumb-itemПункт, если списка <li> нет
.gr-breadcrumb-scrollДлинный путь уезжает в прокрутку, а не переносится

Разделитель рисует ::before у каждого пункта, кроме первого; символ — переменная --gr-breadcrumb-sep (по умолчанию /). Последний пункт не ссылка, а текст с aria-current="page".

Меню (scss/_menu.scss)

КлассОписание
.gr-menuСписок действий: поверхность, рамка, тень
.gr-menu-itemПункт — <button> или <a>
.gr-menu-header / .gr-menu-sepЗаголовок группы и разделитель на <hr>
.gr-menu-dangerОпасное действие
.gr-menu-icon / .gr-menu-shortcutСлот под иконку и под сочетание клавиш

Собственного раскрытия у модуля нет — это оформление списка. Открывает его .gr-dropdown.

Выпадающая панель (scss/_dropdown.scss)

КлассОписание
.gr-dropdownОбёртка: <div> с попапом или сам <details>
.gr-dropdown-panelПанель; внутрь кладут .gr-menu или что угодно ещё
.gr-dropdown-end / -upРаскрытие к концу строки и вверх
.gr-dropdown-hoverРаскрытие по наведению; с клавиатуры и на сенсорном экране — по фокусу
ТокенПо умолчаниюЧитает
--gr-dropdown-min-widthmax-content.gr-dropdown-panel — минимальная ширина панели, в обоих путях (абсолютная и popover)
--gr-dropdown-pad0.gr-dropdown-panel — поле вокруг содержимого; сама панель — коробка позиционирования, рамку и фон даёт .gr-menu внутри

Два пути, оба без скрипта. <details class="gr-dropdown"> работает везде, но не закрывается по Esc и по клику мимо — <details> о них не знает. <button popovertarget> с панелью popover получает и то, и другое от браузера; привязать её к кнопке умеет только CSS anchor positioning, поэтому там, где его нет, панель раскрывается листом у нижнего края экрана. Положение не то — меню доступно и закрывается штатно.

Третий способ — .gr-dropdown-hover: панель держится, пока указатель на обёртке, и уходит с четвертьсекундной задержкой; зазор до панели перекрыт невидимым мостиком, иначе наведение прерывалось бы на полпути. Скрипт не нужен и здесь: показ — :hover внутри @media (hover: hover), а на сенсорном экране и с клавиатуры — :focus-within, поэтому кнопка обязана быть <button>. Цена способа: aria-expanded без скрипта не обновить (кнопке ставят aria-haspopup="true" и молчат об открытости), а закрытия по Esc платформа тут не даёт — наведение для браузера не «открытое состояние». Годится для навигации; меню с «Удалить» и «Выйти» делают на <details> или на попапе, где состояние настоящее.

Аккордеон (scss/_accordion.scss)

КлассОписание
.gr-accordionОбёртка с рамкой и линиями между разделами
.gr-accordion-itemРаздел — <details>
.gr-accordion-triggerЗаголовок — <summary>; стрелка своя, родная снята
.gr-accordion-panelСодержимое раздела
.gr-accordion-flushБез рамки и фона — внутрь карточки или колонки текста
ТокенПо умолчаниюЧитает
--gr-accordion-summary-pad--gr-gap.gr-accordion-trigger — по горизонтали целиком, по вертикали половина

Эксклюзивность (открыт только один раздел) даёт платформа: общий атрибут name у соседних <details>. CSS на это не нужен вовсе.

Плавное раскрытие включается только там, где браузер умеет анимировать высоту до auto (interpolate-size: allow-keywords) и выбирать содержимое <details> селектором (::details-content). Без обоих условий раздел раскрывается сразу — и это рабочее состояние, а не поломка.

Вкладки (scss/_tabs.scss)

КлассОписание
.gr-tabsОбёртка
.gr-tablistРяд заголовков; прокручивается вбок, а не переносится
.gr-tabЗаголовок — ссылка-якорь или кнопка
.gr-tab-panelПанель
.gr-tabs-pills / -stretchЗаливка вместо линии; ряд во всю ширину
ТокенПо умолчаниюЧитает
--gr-tabs-tab-pad--gr-control-padding-x.gr-tab, горизонтальный отступ вкладки

Без скрипта это ссылки-якоря и подряд идущие секции: страница читается и печатается целиком. Со скриптом (griffincss-ui.js) появляются role="tablist", стрелки, Home/End и roving tabindex, а неактивные панели прячет атрибут hidden — не правило библиотеки: display: none убрал бы панель с экрана, но оставил бы её в порядке обхода.

Модальное окно (scss/_modal.scss)

КлассОписание
.gr-modalОкно на <dialog>
.gr-modal-header / -title / -body / -footerПояса; прокручивается только тело
.gr-modal-sm / -lg / -fullРазмеры
ТокенПо умолчаниюЧитает
--gr-modal-body-overflowauto.gr-modal-body, overflow-y: окну с собственной прокруткой внутри (карта, список) — hidden
--gr-modal-transition--gr-transitionДлительность и кривая перехода окна — все четыре свойства (opacity, translate, overlay, display)
--gr-radius-container--gr-radiusСкругление окна — токен ядра, общий с карточкой, меню и сообщением (reference-core.html)

Верхний слой без z-index, удержание фокуса внутри окна, закрытие по Esc, возврат фокуса на кнопку, инертность страницы под окном — всё это даёт <dialog>. Открывает окно showModal(), закрывает <form method="dialog"> внутри него — без единой строки собственного JS.

Единственное, чего <dialog> не умеет, — закрытие щелчком по подложке. Его добавляет опциональный griffincss-ui.js, и включается оно атрибутом на самом окне, а не глобальной настройкой:

<dialog class="gr-modal" data-gr-overlay-close>…</dialog>

<script src="griffincss-ui/dist/griffincss-ui.js"></script>

Работает и на .gr-drawer — это тот же <dialog>. После такого закрытия dialog.returnValue равен "overlay", поэтому обработчик отличит щелчок мимо от кнопки «Сохранить». Выделение текста, начатое внутри окна и законченное за его краем, окно не закрывает: рантайм сверяет обе половины щелчка. У окна с наполовину заполненной формой атрибут просто не ставят.

Выдвижная панель (scss/_drawer.scss)

КлассОписание
.gr-drawerТот же <dialog>, прижатый к краю экрана
.gr-drawer-start / -end / -top / -bottomСторона
.gr-drawer-sm / -lgШирина

Пояса берутся у модалки — .gr-modal-header, -body, -footer работают и здесь: панель отличается положением, а не устройством. Стороны заданы логическими свойствами, поэтому в разметке справа налево панель «конца строки» выезжает слева.

Подсказка (scss/_tooltip.scss)

КлассОписание
.gr-tooltip-anchorОбёртка вокруг элемента и подсказки
.gr-tooltipСама подсказка
.gr-tooltip-bottom / -start / -endСторона (по умолчанию сверху)

Показывается и указателем, и с клавиатуры, и то и другое — чистым CSS: :hover внутри @media (hover: hover) и :focus-within. aria-describedby в разметке обязателен, иначе подсказку получит только тот, кто её видит. Прячут её visibility и прозрачность, а не display: none: скрытая через display, она исчезла бы из дерева доступности вместе со связью.

Пагинация (scss/_pagination.scss)

КлассОписание
.gr-paginationРяд страниц
.gr-pageСтраница — ссылка или кнопка
.gr-page-gapМноготочие: пропуск страниц
.gr-pagination-compact / -joinedПлотный ряд; слипшаяся лента

Текущая страница помечается aria-current="page" и выделяется заливкой, а не только цветом: в ряду одинаковых цифр разница в оттенке замечается хуже, чем разница в весе пятна. Недоступная кнопка «дальше» получает aria-disabled="true" и остаётся в порядке обхода — исчезнувшая кнопка сбивает счёт нажатий Tab, приглушённая не сбивает.

Полоса выполнения и шкала (scss/_progress.scss)

КлассОписание
.gr-progressПолоса на <progress>; значение показывает сам элемент
.gr-progress-sm / -lgТолщина
.gr-progress-success / -warning / -dangerЦвет заполнения
.gr-progress-indeterminateРабота идёт, доля неизвестна: бегущий отрезок
.gr-meterШкала на <meter>; цвет вычисляет браузер

Дорожку и заполнение рисуют вендорные псевдоэлементы, и каждый объявлен своим правилом: неизвестный селектор делает невалидной всю группу, поэтому ::-webkit-progress-value рядом с ::-moz-progress-bar не покрасил бы ни того, ни другого.

У шкалы классов-статусов нет намеренно. <meter> сам вычисляет по low, high и optimum, попало ли значение в хороший диапазон; класс позволил бы разметке сказать «плохо» там, где элемент вычислил «хорошо».

<progress> — про ход задачи, у которой есть конец. <meter> — про измеренную величину: заполненность диска, оценку, запас.

Рейтинг (scss/_rating.scss)

Класс или переменнаяОписание
.gr-ratingПять знаков, залитых по оценке: отзыв о товаре, средний балл в списке. Только отображение — ввод оценки остаётся за потребителем
--gr-ratingОценка от 0 до 5 на самом элементе. Дробная рисуется частью знака: 4,5 — ровно 90 % ширины ряда
--gr-rating-symbolЗнак, токен ядра. По умолчанию звезда, записанная экранированием "\2605": литеральный символ сделал бы собранный CSS не-ASCII, и Sass приписал бы к файлу BOM
--gr-color-ratingЦвет залитого знака — семантический токен темы (reference-core.html): его же читает всё, что изображает оценку рядом с рядом. По умолчанию --gr-color-warning-surface; пустой знак — оттенок currentcolor
--gr-rating-colorПереопределение цвета на одном ряду (или в своём :root): style="--gr-rating-color: var(--gr-color-danger)". В :root библиотеки не объявлена — объявленная там, она резолвилась бы один раз и не следовала бы теме вложенной секции

Два одинаковых ряда знаков друг над другом: нижний (::before) задаёт ширину и рисует пустые знаки, верхний (::after) залит и обрезан по доле оценки. Заливка одного ряда через background-clip: text потребовала бы color: transparent, а прозрачный текст исчезает в режиме принудительных цветов вместе со всей оценкой.

Разрядки между знаками нет намеренно: интервал сдвигал бы границу заливки на целое число промежутков, и доля перестала бы совпадать с оценкой. Обрезка идёт от логического края (inset-inline-start), поэтому под dir="rtl" ряд разворачивается сам — правил на [dir] в модуле нет.

Иконок библиотека не везёт: вид задаётся глифом в --gr-rating-symbol, и на этом граница. Оценка обязана быть и словом — role="img" с aria-label вида «4,5 из 5».

Кольцо занятости (scss/_spinner.scss)

КлассОписание
.gr-spinnerКольцо; диаметр 1.5em от кегля родителя
.gr-spinner-sm / -lgМеньше и крупнее
.gr-spinner-currentКольцо цветом текста — для цветной кнопки

Размеры в em, а не в rem: кольцо внутри кнопки должно быть по её кеглю. Пустому <span> нужны role="status" и подпись, иначе о загрузке узнает только тот, кто видит кольцо; рядом с видимым словом «Сохраняем…» хватает aria-hidden="true".

При prefers-reduced-motion вращение замедляется, но не снимается: остановленный индикатор занятости сообщает «зависло».

Заглушка на время загрузки (scss/_skeleton.scss)

КлассОписание
.gr-skeletonПрямоугольник на месте будущего содержимого
.gr-skeleton-textСтрока высотой в кегль; последняя в блоке короче
.gr-skeleton-circleКруг под аватар
.gr-skeleton-groupКоробка заглушки: свои отступы и зазоры между фигурами
.gr-skeleton-stackКолонка строк внутри коробки

Залито оттенком currentcolor, а не серым: непрозрачный цвет совпал бы с одной из подложек и стал бы невидимым. .gr-skeleton-group задаёт отступы и зазоры сама, поэтому в карточке встаёт вместо .gr-card-body — у самой .gr-card внутренних отступов нет. Обёртке нужен aria-hidden="true", а рядом — слово в role="status": пустые прямоугольники в озвучке не значат ничего.

Мерцание по prefers-reduced-motion снимается совсем — в отличие от спиннера: заглушка сообщает о загрузке самим своим присутствием.

Всплывающие уведомления (scss/_toast.scss)

КлассОписание
.gr-toast-regionЖивая область; aria-live и роль задаёт разметка
.gr-toast-region-top-start / -top-end / -bottom-start / -bottom-endУгол; у нижних колонка перевёрнута
.gr-toastСообщение
.gr-toast-info / -success / -warning / -dangerСтатус
.gr-toast-titleСлово статуса
.gr-toast-icon / -bodyСлот под знак и текст
.gr-toast-leavingУход; класс вешает рантайм

Единственный компонент библиотеки без статического применения: показывает и убирает тосты griffincss-ui.js.

<div class="gr-toast-region gr-toast-region-bottom-end"
     role="status" aria-live="polite"></div>
Griffincss.ui.toast('Черновик сохранён', { status: 'success', title: 'Готово' });

Регион ставят на страницу один раз и пустым: программа чтения с экрана следит за изменениями внутри aria-live, а не за появлением самой области. Появление тоста объявлено через @starting-style, поэтому рантайму не нужны ни второй кадр, ни пара «добавил класс — снял класс».

Шаги процесса (scss/_steps.scss)

КлассОписание
.gr-stepsРяд шагов; разметка — <ol>
.gr-steps-verticalТот же ряд колонкой
.gr-stepШаг; по умолчанию — ожидающий
.gr-step-doneПройденный: линия от него к следующему залита
.gr-step-disabledПропущенный или отменённый
.gr-step-marker / -label / -note / -bodyКружок со знаком, название, пояснение, колонка подписи

Текущий шаг помечается aria-current="step" — тем же признаком, по которому его находит программа чтения с экрана; класса для него нет. Знак в маркере (номер, галочка) кладут в разметку: цвет не переживает чёрно-белую печать и дальтонизм, знак переживает.

Линия — хвост шага, ведущий к следующему: ::after у левого, в колонке — у верхнего. Залита она у пройденного, у текущего остаётся серой. Так решено потому, что только у левого шага длину линии считает сам CSS: сколько текста висит под маркером предыдущего шага, правило, написанное на следующего, знать не может, и в колонке хвост, растущий вверх, не доставал бы до соседнего маркера.

Пустое состояние (scss/_empty.scss)

КлассОписание
.gr-emptyКолонка по центру: иконка, заголовок, пояснение, действие
.gr-empty-boxedТо же внутри пунктирной рамки
.gr-empty-icon / -title / -text / -actionsЧасти

Ширина пояснения ограничена в ch, а не в пикселях, поэтому ограничение в 46 знаков остаётся таким же и в режиме для слабовидящих. Слот под кнопку есть потому, что пустое состояние без действия — тупик.

Рантайм компонентов (dist/griffincss-ui.js)

ES5-совместимый файл без зависимостей, пишется руками и не компилируется. Автостарт по DOMContentLoaded; отключается атрибутом data-auto="false" на теге <script>. Делает четыре вещи, которых платформа не даёт:

ЧтоКак
Закрытие окна щелчком по подложкеdata-gr-overlay-close на <dialog>; returnValue"overlay"
Крестик[data-gr-dismiss] внутри .gr-alert, .gr-toast или .gr-tag; значение атрибута — селектор цели, если она снаружи
ТостыGriffincss.ui.toast(text, options)
Клавиатура вкладокdata-gr-tabs на обёртке: роли, aria-selected, стрелки, Home/End, roving tabindex, hidden у неактивных панелей
Параметр toastПо умолчаниюЧто делает
statusнетinfo / success / warning / danger; danger переводит область в assertive
titleнетСлово статуса; статус без слова — предупреждение в консоль
timeout5000Миллисекунды до ухода; 0 — ждать крестика
positionbottom-endОдин из четырёх углов
iconнетЗнак в слот .gr-toast-icon
dismissibletruefalse убирает крестик
assertiveпо статусуПрямое указание, какой областью показать

Таймер тоста останавливается под указателем и под фокусом и дочитывает остаток, а не начинается заново. Все обработчики — делегированные, по одному на документ: Griffincss.ui.start() и .destroy() поднимают и снимают их целиком.

Клавиатура вкладок включается только при data-gr-tabs. Без атрибута и без скрипта та же разметка остаётся списком ссылок-якорей и секциями подряд — страница читается сверху вниз и печатается целиком.

События рантайма

Рантайм сообщает о действиях пользователя событиями CustomEvent — по образцу griffincss:themechange у рантайма темы. События всплывают, слушать их можно на документе.

СобытиеКогдаdetail
griffincss:tabchangeПользователь переключил вкладку щелчком или с клавиатуры; расстановка ролей при инициализации события не шлётindex, tab, panel
griffincss:toastТост показан вызовом Griffincss.ui.toast()node, status (null без статуса), position