Слои поверх страницы
Выпадающая панель, меню, модальное окно, выдвижная панель и подсказка. Верхний слой, фокус, Esc и закрытие по клику мимо — всё от платформы.
Ни один из пяти модулей не требует библиотечного JS.
<details> раскрывает панель, <dialog>
держит фокус внутри окна и закрывается по Esc, Popover API
закрывает панель по клику мимо, а подсказка появляется по
:hover и :focus-within чистым CSS. Скрипт нужен
ровно на одно: вызвать showModal() — это одна строка
в разметке.
Единственное исключение — закрытие окна щелчком по подложке: этого
<dialog> не умеет, и его добавляет опциональный
griffincss-ui.js. Он подключён на этой странице, включается
атрибутом на самом окне и стоит 2,9 КБ gzip — вместе
со всем остальным поведением компонентов.
Выпадающая панель и меню
| Класс | Назначение |
|---|---|
.gr-dropdown | Обёртка: <div> с попапом или сам <details> |
.gr-dropdown-panel | Панель; внутрь кладут .gr-menu или что угодно ещё |
.gr-dropdown-end / -up | Раскрытие к концу строки и вверх |
.gr-dropdown-hover | Раскрытие по наведению; с клавиатуры и на сенсорном экране — по фокусу |
.gr-menu | Список действий: поверхность, рамка, тень |
.gr-menu-item | Пункт — <button> или <a> |
.gr-menu-header / .gr-menu-sep | Заголовок группы и разделитель на <hr> |
.gr-menu-danger | Опасное действие |
.gr-menu-icon / .gr-menu-shortcut | Слоты под иконку и сочетание клавиш |
<details> — работает вездеДействия
К концу строки
<details class="gr-dropdown">
<summary class="gr-btn">Действия</summary>
<div class="gr-dropdown-panel">
<ul class="gr-menu">
<li><button class="gr-menu-item" type="button">Переименовать</button></li>
<li><hr class="gr-menu-sep"></li>
<li><button class="gr-menu-item gr-menu-danger" type="button">Удалить</button></li>
</ul>
</div>
</details>
<div class="gr-dropdown">
<button class="gr-btn" type="button" popovertarget="m">Экспорт</button>
<div class="gr-dropdown-panel" id="m" popover>
<ul class="gr-menu">…</ul>
</div>
</div>
.gr-dropdown-hover<div class="gr-dropdown gr-dropdown-hover">
<button class="gr-btn gr-btn-ghost" type="button" aria-haspopup="true">Продукты</button>
<div class="gr-dropdown-panel">
<ul class="gr-menu">…</ul>
</div>
</div>
Панель держится, пока указатель на обёртке, и уходит с четвертьсекундной
задержкой — соскользнувший мимо курсор не захлопывает меню мгновенно.
Зазор между кнопкой и панелью перекрыт невидимым мостиком, иначе
наведение прерывалось бы на полпути. Ни одной строки JS: показ —
:hover, а на сенсорном экране и с клавиатуры —
:focus-within, поэтому кнопка обязана быть
<button>.
Наведение годится для навигации, а не для действий.
Состояние такого меню разметкой не сообщается: aria-expanded
без скрипта не обновить, поэтому кнопке ставят
aria-haspopup="true" и не ставят aria-expanded —
промолчать лучше, чем солгать об открытости. Закрытия по Esc
платформа здесь тоже не даёт: наведение для браузера не «открытое
состояние». Меню с «Удалить» и «Выйти» делают на
<details> или на попапе, где состояние настоящее.
Два пути, и оба чего-то стоят. <details>
работает в любом браузере, но не знает ни Esc, ни клика мимо —
закрывать панель придётся тем же нажатием на заголовок. Панель с атрибутом
popover получает и то, и другое от браузера даром, но уходит
в верхний слой, где положение считается от окна, а не от обёртки: привязать
её к кнопке умеет только CSS anchor positioning. Там, где его нет, панель
раскрывается листом у нижнего края экрана — положение не то, зато меню
доступно и закрывается штатно. Ни один из путей не оставляет пользователя
с неработающей кнопкой.
Модальное окно
| Класс | Назначение |
|---|---|
.gr-modal | Окно на <dialog> |
.gr-modal-header / -title | Шапка и заголовок, на который указывает aria-labelledby |
.gr-modal-body | Тело; прокручивается только оно |
.gr-modal-footer | Подвал с кнопками |
.gr-modal-sm / -lg / -full | Размеры |
[data-gr-state="loading"] | Состояние загрузки на самом <dialog>: кольцо по центру, пока содержимое не пришло. Рядом уместен aria-busy="true" |
<button type="button" onclick="m.showModal()">Открыть</button>
<dialog class="gr-modal" id="m" aria-labelledby="m-title">
<div class="gr-modal-header">
<h2 class="gr-modal-title" id="m-title">Заголовок</h2>
</div>
<div class="gr-modal-body">…</div>
<div class="gr-modal-footer">
<form method="dialog"><button class="gr-btn">Закрыть</button></form>
</div>
</dialog>
Пустое окно: содержимое приходит позже
Типичный сценарий витрины: окно открывается сразу, а форму обратного
звонка, быстрый заказ или выбор города в него кладёт ответ сервера.
До ответа <dialog> пуст — и раньше рисовался
полоской высотой в рамку, которая через полсекунды рывком раскрывалась
в окно. Теперь у окна минимальная высота 10rem: пустое
окно — плашка, а состояние data-gr-state="loading" рисует
в её центре кольцо тем же keyframe, что у
.gr-spinner. Ставит и снимает состояние тот же скрипт,
что грузит содержимое; программе чтения с экрана о том же говорит
aria-busy="true".
const m = document.getElementById('callback');
m.dataset.grState = 'loading';
m.setAttribute('aria-busy', 'true');
m.showModal();
const html = await (await fetch('/callback-form')).text();
m.innerHTML = html;
delete m.dataset.grState;
m.removeAttribute('aria-busy');
Плавный рост высоты с приходом содержимого — не работа CSS: высота
окна auto и до, и после, а переход между двумя
auto не анимируется. Это делает слой
griffinjs.js по атрибуту data-gr-dialog="grow"
на окне — см. подраздел
«С подключённым griffinjs.js».
Закрытие щелчком по подложке
Единственное, чего <dialog> не умеет сам. Включается
атрибутом на самом окне — не глобальной настройкой:
<dialog class="gr-modal" data-gr-overlay-close>…</dialog>
<script src="griffincss-ui/dist/griffincss-ui.js"></script>
| Что | Как |
|---|---|
data-gr-overlay-close | Атрибут на <dialog>: окно или панель закрывается щелчком мимо |
dialog.returnValue | После такого закрытия равно "overlay" — обработчик отличит щелчок мимо от кнопки «Сохранить» |
Griffincss.ui.start() / .destroy() | Ручной запуск и снятие обработчиков; автостарт отключается data-auto="false" на теге <script> |
Оба демонстрационных окна выше и три из четырёх выдвижных панелей помечены этим атрибутом — щёлкните мимо, окно закроется. Рантайм вешает один делегированный обработчик на документ: окна появляются и исчезают вместе с разметкой, и переподписываться на каждое не нужно.
Атрибут — решение автора, а не умолчание. У окна с наполовину заполненной формой случайный щелчок мимо стоит потерянных данных, поэтому поведение включается там, где оно уместно: у просмотрщика картинки, у меню на телефоне, у окна с одной кнопкой «Понятно».
Выделение текста, начатое внутри окна и законченное за его краем, окном не считается щелчком по подложке — рантайм сверяет обе половины щелчка. Не закрывают окно и клавиатурный Enter на кнопке, и щелчок по неотрисованному окну.
Закрытие вообще без скрипта. Кнопка внутри
<form method="dialog"> закрывает окно сама — это поведение
формы, а не обработчик. Именно так закрываются оба окна на этой странице:
ни одной строки JS на закрытие не написано, скрипт вызывает только
showModal().
Выдвижная панель
| Класс | Назначение |
|---|---|
.gr-drawer | Тот же <dialog>, прижатый к краю экрана |
.gr-drawer-start / -end | К началу и к концу строки |
.gr-drawer-top / -bottom | Сверху и снизу; высота по содержимому, но не больше половины экрана |
.gr-drawer-sm / -lg | Ширина: 15rem / 28rem |
<dialog class="gr-drawer gr-drawer-end" aria-label="Фильтры">
<div class="gr-modal-header">…</div>
<div class="gr-modal-body">…</div>
</dialog>
Пояса панель не заводит свои: .gr-modal-header,
-body и -footer работают и здесь. Дублировать их
незачем — от модалки панель отличается положением и размерами,
а не устройством.
Подсказка
| Класс | Назначение |
|---|---|
.gr-tooltip-anchor | Обёртка вокруг элемента и подсказки |
.gr-tooltip | Сама подсказка; по умолчанию сверху |
.gr-tooltip-bottom / -start / -end | Другая сторона |
<span class="gr-tooltip-anchor">
<button class="gr-btn" type="button" aria-describedby="t1">Сохранить</button>
<span class="gr-tooltip" id="t1" role="tooltip">Ctrl + S</span>
</span>
aria-describedby обязателен. Без него подсказку
получит только тот, кто её видит: программе чтения с экрана достанется
кнопка «Сохранить» без единого слова о сочетании клавиш. И прячется
подсказка не display: none, а прозрачностью с
visibility: убранная через display, она исчезла бы
из дерева доступности вместе со связью, на которую ссылается кнопка.
Примеры использования
Слои собираются из уже показанных частей: меню — из дропдауна и аватара, корзина — из панели и медиа-объектов. Оба рецепта живые.
Меню пользователя в шапке
По клику на аватар — панель к концу строки: шапка-группа с именем
и почтой (.gr-menu-header двумя строками), пункты,
разделитель и «Выйти» в .gr-menu-danger. Всё
на <details> — здесь есть опасное действие, поэтому
раскрытие по наведению не годится: нужно настоящее состояние.
<details class="gr-dropdown gr-dropdown-end">
<summary class="gr-btn gr-btn-ghost gr-btn-icon" aria-label="Меню пользователя">
<span class="gr-avatar gr-avatar-sm">АК</span>
</summary>
<div class="gr-dropdown-panel">
<ul class="gr-menu">
<li><div class="gr-menu-header">Анна Ковалёва<br>
<span class="gr-text-xs gr-text-ink-secondary gr-font-normal">anna@example.ru</span></div></li>
<li><a class="gr-menu-item" href="/profile">Профиль</a></li>
<li><a class="gr-menu-item" href="/settings">Настройки</a></li>
<li><hr class="gr-menu-sep"></li>
<li><button class="gr-menu-item gr-menu-danger" type="button">Выйти</button></li>
</ul>
</div>
</details>
АК
Корзина в выдвижной панели
«Товар добавлен» — и справа выезжает корзина: .gr-drawer
.gr-drawer-end. Внутри позиции — медиа-объекты со стопкой
«название + цена» и кнопкой-крестиком, в подвале —
.gr-flex-between с итогом и кнопка оформления
на всю ширину. Контекст страницы не теряется — панель поверх,
щелчок мимо закрывает её (data-gr-overlay-close).
<dialog class="gr-drawer gr-drawer-end" id="cart" aria-label="Корзина" data-gr-overlay-close>
<div class="gr-modal-header">
<strong>Корзина</strong>
<form method="dialog"><button class="gr-btn gr-btn-ghost gr-btn-icon" aria-label="Закрыть">✕</button></form>
</div>
<div class="gr-modal-body gr-flex gr-flex-col gr-gap">
<div class="gr-flex gr-gap-sm gr-flex-items-start">
<span class="gr-avatar gr-avatar-square">🪑</span>
<div class="gr-min-w-0 gr-flex-1">
<p class="gr-m-0 gr-text-sm gr-font-medium">Кресло «Дюна»</p>
<p class="gr-m-0 gr-text-xs gr-text-ink-secondary">1 × 12 400 ₽</p>
</div>
<button class="gr-btn gr-btn-icon gr-btn-ghost gr-btn-sm">✕<span class="gr-sr-only">Убрать</span></button>
</div>
</div>
<div class="gr-modal-footer gr-flex gr-flex-col gr-gap-sm">
<div class="gr-flex gr-flex-between gr-w-full">
<span>Итого</span><span class="gr-font-bold">18 300 ₽</span>
</div>
<button class="gr-btn gr-btn-primary gr-btn-block">Оформить заказ</button>
</div>
</dialog>
С подключённым griffinjs.js
Всё выше работает без скрипта. Этот подраздел — о том, что добавляет
опциональный слой GriffinJS к тем же
компонентам: разметка и классы не меняются, скрипт читает
data-gr-* и пишет только aria-*
и data-gr-state. Каждый пример здесь помечен бейджем
«нужен griffinjs.js»; без скрипта он остаётся обычным
компонентом из основной части страницы.
Дропдаун: роли меню, клавиатура, закрытие, позиция
data-gr-dropdown на обёртке любой из трёх форм —
<details>, popover, наведение. Контроллер ставит роли
menu/menuitem/separator, честный
aria-expanded, ведёт клавиатуру по схеме WAI-ARIA menu
button (↓ открывает и переходит к первому пункту,
↑ ↓ Home End по пунктам, набор
по первой букве, Esc закрывает и возвращает фокус) и добавляет
<details> то, чего у него нет: закрытие по Esc
и щелчку мимо. Popover-панель контроллер кладёт под кнопку сам — по
getBoundingClientRect, одинаково во всех браузерах, без
зависимости от CSS anchor positioning; при нехватке места снизу
переворачивает вверх, при прокрутке — догоняет кнопку. Закрытие
<details> идёт с анимацией темы: обёртка получает
data-gr-state="closing" и держит open до конца
перехода панели (рецепт — на странице
мегаменю).
<details class="gr-dropdown" data-gr-dropdown>…</details>
<div class="gr-dropdown" data-gr-dropdown="align: end">
<button popovertarget="m">…</button><div id="m" popover>…</div>
</div>
<div class="gr-dropdown gr-dropdown-hover" data-gr-dropdown="hover; delay: 100">…</div>
Действия
Параметр data-gr-dropdown | По умолчанию | Что делает |
|---|---|---|
role | menu | Роли списка; none — панель с формой или произвольным содержимым, ролей и aria-haspopup нет |
hover | false | Открывать по наведению мыши и пера с задержкой delay, закрывать через hide после ухода |
delay / hide | 100 / 300 | мс задержки намерения и ухода |
side / align / gap | bottom / start / 4 | Только для popover-панели: сторона, выравнивание вдоль неё, зазор в px |
С контроллером у варианта по наведению aria-expanded наконец
правдив, а Esc закрывает панель — два ограничения из
предупреждения выше сняты. Третье остаётся решением автора: наведение
для навигации, не для действий.
Окно и панель: открытие атрибутом, стек, прокрутка, медиа, ajax, hash
Контроллер окон ничего не переписывает у <dialog>:
верхний слой, фокус, Esc и inert остаются
платформе, щелчок по подложке — атрибуту data-gr-overlay-close
выше. Он добавляет data-gr-open="#id" на кнопке вместо
onclick, стек окон, блокировку прокрутки страницы под окном
(в том числе на iOS), паузу видео при закрытии, содержимое с сервера
по data-gr-src, синхронизацию с адресом
(data-gr-dialog="hash") и плавный рост окна с приходом
содержимого (data-gr-dialog="grow"). Подробно — на странице
GriffinJS → «Окна и комбобокс».
<button class="gr-btn" type="button" data-gr-open="#cart">Корзина</button>
<dialog class="gr-drawer gr-drawer-end" id="cart" data-gr-dialog="hash" data-gr-overlay-close>…</dialog>
Подсказка в верхнем слое
Обычной подсказке скрипт не нужен. Он нужен подсказке внутри таблицы
с прокруткой или блока с overflow: hidden — её обрезает край
предка. data-gr-tooltip на обёртке переводит подсказку
в popover="manual", показывает по наведению с задержкой
(мышь и перо) и по фокусу сразу, прячет по уходу, потере фокуса
и Esc, а положение у якоря считает сам. Без скрипта та же
разметка — обычная подсказка из раздела выше; aria-describedby
работает в обоих случаях.
<span class="gr-tooltip-anchor" data-gr-tooltip="side: bottom">
<button class="gr-btn" type="button" aria-describedby="t9">В таблице</button>
<span class="gr-tooltip" id="t9" role="tooltip">Не обрежется</span>
</span>
overflow: hidden высотой в одну кнопку нужен griffinjs.jsПараметр data-gr-tooltip | По умолчанию | Что делает |
|---|---|---|
delay | 150 | мс до показа по наведению; по фокусу — сразу |
side / align / gap | top / center / 6 | Сторона, выравнивание, зазор в px; при нехватке места сторона переворачивается |