Перейти к содержимому
EN
Масштаб
Плотность
Тема
Акцент

Оверлеи

Модалка

JS Открытие — одна строка: dlg.showModal(). Закрытие, подложка, блокировка прокрутки фона и возврат фокуса скрипта не требуют

Окно, которое забирает фокус и гасит фон, потому что без ответа продолжать нельзя. Стилизуется нативный <dialog>, поэтому модальность настоящая, а не нарисованная.

Компонент
Удалить прогон #4127? Артефакты уйдут вместе с ним

Будут удалены лог, артефакты и диф. Ссылки на прогон в отчётах перестанут открываться.

Необратимо: восстановление из резервной копии занимает до суток.
Escape тоже закрывает
Разметка
<button class="inst-btn inst-btn--danger" type="button"
        onclick="document.getElementById('dlg-confirm').showModal()">Удалить прогон</button>

<dialog class="inst-dialog" id="dlg-confirm">
  <form method="dialog">
    <div class="inst-dialog-head">
      <span class="inst-dialog-title">Удалить прогон #4127?</span>
      <span class="inst-dialog-sub">Артефакты уйдут вместе с ним</span>
      <button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon inst-dialog-close" type="submit" aria-label="Закрыть">
        <svg class="inst-icon" aria-hidden="true"><use href="#i-close"/></svg>
      </button>
    </div>
    <div class="inst-dialog-body inst-stack">
      <p>Будут удалены лог, артефакты и диф. Ссылки на прогон в отчётах перестанут открываться.</p>
      <div class="inst-note" data-tone="warn">Необратимо: восстановление из резервной копии занимает до суток.</div>
    </div>
    <div class="inst-dialog-foot">
      <span class="inst-dialog-foot-note">Escape тоже закрывает</span>
      <button class="inst-btn" type="submit">Отмена</button>
      <button class="inst-btn inst-btn--danger" type="submit">Удалить</button>
    </div>
  </form>
</dialog>

Контракт

Всё содержимое обёрнуто в <form method="dialog">. Открытие — одна строка скрипта; закрытие, подложка и возврат фокуса скрипта не требуют.

onclick в примере выше — сокращение ради одного файла. В приложении обработчик вешается кодом; способ его повесить библиотеке безразличен. Декларативные command и commandfor пока прогрессивны и в контракт библиотеки не берутся.

Доступность

Фокус showModal() уводит фокус внутрь, держит его там и возвращает на вызвавшую кнопку при закрытии. Ничего из этого писать не надо
Клавиатура Escape закрывает. Tab не выходит за пределы модалки
Фон инертен Содержимое под подложкой недоступно ни мыши, ни скринридеру — это делает платформа
Прокрутка фона Останавливается правилом html:has(dialog:modal). Иначе пользователь теряет место
Заголовок inst-dialog-title оформляет, но не объявляет. Если модалке нужно доступное имя, свяжите её aria-labelledby с заголовком
Подложка --scrim — 0.32 в светлой теме и 0.58 в тёмной: фон читается как выключенный, но не исчезает
Печать Модалка на листе не печатается
Обязательная разметка Нативный <dialog>, showModal() вместо show(), <form method="dialog"> внутри, type="submit" у закрывающих кнопок, aria-label у крестика

Устройство

Часть Работа
inst-dialog-head Шапка: заголовок и крестик. Отделена волоском
inst-dialog-title Заголовок, --text-md
inst-dialog-close Крестик. Прижимается к дальнему краю шапки
inst-dialog-body Тело. Единственная прокручиваемая часть
inst-dialog-foot Подвал с действиями
inst-dialog-foot--start Действия у ближнего края. Умолчание — дальний
inst-dialog-foot-note Пояснение у ближнего края, отжимающее кнопки к дальнему

Шапка и подвал не сжимаются (flex: none), прокручивается только тело. Из-за этого заголовок и кнопки остаются на месте на любой длине содержимого, а модалка не вырастает выше 80dvh.

Свойство Значение
ширина min(34rem, 100vw - var(--space-8))
максимальная высота min(80dvh, 100dvh - var(--space-8))

Раскладка задаётся только открытому: .inst-dialog[open] { display: flex }. Безусловный display: flex показал бы модалку всегда — у <dialog> закрытое состояние это display: none.

Модалка — второй и последний носитель тени в библиотеке. Первый — поповер.

Поведение

Закрытие без скрипта

Любая кнопка type="submit" внутри <form method="dialog"> закрывает модалку и передаёт своё значение в dialog.returnValue — обработчик клика не нужен ни на одной из них, включая крестик и «Отмена».

Способ закрытия Что нужно
Кнопка внутри <form method="dialog"> type="submit"
Escape Ничего. Платформа
Клик по подложке closedby="any" на <dialog>

Блокировка прокрутки фона

showModal() делает фон инертным для кликов, но не останавливает прокрутку: колесо продолжает крутить страницу, и пользователь теряет место, к которому вернётся. Библиотека закрывает это одним правилом:

html:has(dialog:modal) { overflow: hidden; }

Селектор :modal, а не [open], и разница здесь принципиальная. Атрибут open стоит и на немодальном show(), у которого вся работа в том, чтобы не блокировать страницу. Пока правило смотрело на атрибут, немодальная панель замораживала прокрутку всего документа — и делала это с любым <dialog> на странице, даже без единого класса библиотеки.

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

Плата известна: если прокручивается сам документ, в момент открытия исчезает полоса прокрутки и содержимое сдвигается вбок на её ширину. scrollbar-gutter здесь не спасает — резерв места действует при overflow: auto/scroll, а тут hidden.

/* Если у вас прокручивается именно документ */
html { scrollbar-gutter: stable; }

В приложении на оболочке документ не прокручивается вообще, поэтому сдвига нет и платить не за что. Библиотека не ставит scrollbar-gutter сам: на нескроллящемся экране это была бы пустая полоса в 10px у правого края.

JS

Открытие — одна строка. Всё остальное берёт на себя платформа.

document.getElementById('dlg').showModal();
Что Кто делает
Открытие Приложение: showModal()
Закрытие <form method="dialog"> внутри — без скрипта
Подложка, верхний слой, Escape Платформа
Возврат фокуса на открывшую кнопку Платформа
Блокировка прокрутки фона Библиотека, правилом html:has(dialog:modal)

Декларативные command и commandfor избавили бы и от этой строки, но пока поддержаны не везде и в контракт библиотеки не берутся.

API

Классы
ИмяЧто делает
класс
inst-dialogБазовый. Ставится на <dialog>
inst-dialog-headШапка
inst-dialog-titleЗаголовок
inst-dialog-closeКрестик у дальнего края шапки
inst-dialog-bodyПрокручиваемое тело
inst-dialog-footПодвал с действиями
inst-dialog-foot-noteПояснение, отжимающее кнопки к дальнему краю
inst-sheetДругая раскладка того же <dialog> — [шторка](./sheet.md)
inst-dialog-subПодпись под заголовком: чего диалог хочет, если из заголовка это не следует целиком
модификатор
inst-dialog-foot--startДействия у ближнего края. Умолчание — дальний
inst-dialog--scrollТело прокручивается: возвращает линейки под шапкой и над подвалом
Токены
ИмяЗначениеОткуда
токен
--surface-overlaylight-dark(var(--n-0), var(--n-11))база +1: тема
--shadow-modal0 16px 40px -8px var(--shadow-color-far), 0 4px 10px -4px var(--shadow-color-near)одно на все ячейки
--scrimlight-dark(oklch(0 0 0 / 0.32), oklch(0 0 0 / 0.58))одно на все ячейки
--borderlight-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11))одно на все ячейки
--border-subtlelight-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06))одно на все ячейки
--hairline1pxбаза +1: экран
--radius-lg12pxбаза +4: масштаб
--pad-cardvar(--space-6)база +14: плотность · масштаб
--space-36pxодно на все ячейки
--space-512pxодно на все ячейки
--space-832pxодно на все ячейки
--gap-inlinevar(--space-4)база +14: плотность · масштаб
--text-md1remбаза +4: масштаб
--text-xs0.78125remбаза +4: масштаб
--weight-medium600одно на все ячейки
--text-mutedlight-dark(var(--n-8), var(--n-6))одно на все ячейки

Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument