Модалки и выезжающие панели
1) Когда что использовать
Modal (диалог с бэкдропом) — для критичных решений и коротких задач, требующих полного внимания: подтверждение действия, правовые согласия, опасные операции, краткие формы ≤ 1–2 полей. Блокирует фон.
Drawer / Sheet (выезжающая панель) — для контекстного расширения: детали объекта, редактирование атрибутов, выбор из списка, вспомогательная навигация. Фон виден → сохраняется контекст.
- Если действию нужна концентрация и подтверждение → Modal.
- Когда нужно сохранить контекст и дать «параллельный» обзор → Drawer.
2) Структура и размеры
Modal
Заголовок (обязателен) → основной текст → CTA-зона (Primary / Secondary / Destructive).
Размеры: S (480–560 px), M (640–720 px), L (≤ 840 px). На мобилке — полноэкранный sheet.
Drawer / Sheet
Направление: правый край (десктоп, редактирование), низ (мобайл, действия), иногда левый (навигация).
Ширина: 360–480 (S), 480–640 (M), 640–800 (L). На мобилке: 90–100% ширины/высоты.
Высота контента всегда ограничена, внутри — скролл; заголовок/CTA фиксированы.
3) Копирайтинг и CTA
Заголовок = действие/смысл: «Подтвердить ставку», «Выбор платежного метода», «Требуется KYC».
Текст краткий, 1–2 предложения. Избегайте расплывчатых формул.
CTA: один Primary, рядом Secondary («Отмена») и, при необходимости, Destructive.
Для рискованных действий добавляйте пояснение в 1 строку: «Действие необратимо. Вы сможете отменить в течение 10 сек (если доступно).»
4) Поведение и состояния
Открытие: мгновенный отклик ≤ 100 мс, затем анимация 120–180 мс.
Закрытие: быстрее открытия (80–140 мс), возвращаем фокус к триггеру.
Busy: `aria-busy="true"` на контейнере, кнопка с блокировкой повторов.
Unsaved (грязная форма): при закрытии — диалог-предупреждение («Есть несохраненные изменения»).
Escape/клик по фону: допустимы для неопасных диалогов; для критичных — только явные кнопки.
5) Доступность (A11y)
Контейнер: `role="dialog"` и `aria-modal="true"` (для настоящей модалки).
Заголовок связан через `aria-labelledby`; описание — `aria-describedby`.
Focus trap внутри; первичный фокус — на заголовке или первом интерактивном элементе.
Возврат фокуса на исходный триггер после закрытия.
Никакого скролла фона: `document.body { overflow: hidden; }` или `inert` на остальном DOM.
Поддержка клавиатуры: Tab/Shift+Tab цикличны; Esc закрывает (если не запрещено сценарно).
Учитывайте `prefers-reduced-motion`: отключение/упрощение анимаций.
html
<div class="backdrop" data-open hidden></div>
<div class="dialog" role="dialog" aria-modal="true" aria-labelledby="d-title" aria-describedby="d-desc" hidden>
<h2 id =" d-title "> Confirm Bid </h2>
<p id =" d-desc "> Sum of 200 ₴ by factor 1. 85</p>
<div class="actions">
<button class =" btn btn--primary "> Confirm </button>
<button class =" btn btn--ghost "> Cancel </button>
</div>
</div>
6) Перформанс и архитектура
Рендер через портал (слой поверх приложения) → меньше проблем со z-index.
Монтируйте контент лениво при первом открытии, демонтируйте после анимации закрытия (или переводите offscreen).
Анимируйте только `transform/opacity`; избегайте дорогих blur/теней с большими размерами.
Блокируйте скролл фона (scroll-lock), сохраняйте текущую позицию, чтобы после закрытия не «прыгало».
Для больших списков в drawer — используйте виртуализацию.
7) Мобильные паттерны
Bottom sheet для быстрых действий/подтверждений: жесты свайпа вниз для закрытия (с порогом).
Sticky-CTA внизу; кнопка закрытия — слева сверху.
Safe-area отступы (notch/gesture areas).
Экранная клавиатура не должна перекрывать CTA; layout — «подъем» контента или фиксированная панель над клавиатурой.
8) Motion-дизайн
Вход: fade + легкий сдвиг (modal: по Y, drawer: по оси появления). 120–180 мс.
Выход: короче (80–140 мс), easing `cubic-bezier(0.2,0,0.2,1)`.
Фон (backdrop): непрозрачность 0 → 0.4–0.6. Без пульсаций и бесконечных бликов.
Для `prefers-reduced-motion`: без сдвига, только fade.
9) Управление закрытием
Немедленное закрытие только при безопасных операциях.
При ошибке — остаемся в диалоге, показываем причину и Retry.
При фоновом выполнении — закрыть диалог и показать тост «Выполняем в фоне…», плюс раздел «История».
10) Типовые сценарии iGaming
10.1 Подтверждение ставки (Modal)
Содержимое: событие, коэффициент, сумма, потенциальный выигрыш, срок действия коэффициента.
Кнопки: «Подтвердить» (primary), «Отмена».
Паттерн задержки > 3 с: текст «Ожидаем подтверждение…»; при изменении коэффициента — честный апдейт.
10.2 Кэшаут (Modal/Sheet)
Показ текущей суммы кэшаута и таймер окна.
Подтверждение + возможный Undo (если регламент позволяет).
10.3 Выбор платежного метода (Drawer)
Список методов с комиссиями/ETA; выбор → мини-формы.
Сохранение метода по умолчанию; возврат без потери введенных данных.
10.4 KYC (Drawer → Modal)
Drawer для загрузки документов/подсказок.
Modal при попытке закрыть с незавершенной загрузкой: предупреждение об несохраненных.
10.5 Лимиты ответственной игры (Modal)
Радио «День/Неделя/Месяц», поле суммы, строка «Вступит в силу через…».
11) Анти-паттерны
Вложенные модалки (modal поверх modal). Используйте один диалог или последовательность шагов.
Модалка для обычного просмотра контента (лучше drawer/страница).
Скрытый крестик или закрытие только по «микрозоне».
Рискованное действие → разрешение закрыть «по фону».
Длиннющая форма в модалке (→ вынесите в отдельный экран/панель).
Отсутствие возврата фокуса к триггеру.
12) Токены дизайн-системы (пример)
json
{
"dialog": {
"radius": 12,
"shadow": "var(--elev-4)",
"sizes": { "s": 520, "m": 680, "l": 840 },
"backdropOpacity": 0. 5,
"padding": "20 24",
"gap": 16
},
"drawer": {
"width": { "s": 360, "m": 480, "l": 640 },
"edge": "right",
"radius": 12,
"shadow": "var(--elev-4)"
},
"motion": {
"inMs": 160,
"outMs": 120,
"ease": "cubic-bezier(0. 2,0,0. 2,1)",
"reduce": true
},
"a11y": {
"useAriaModal": true,
"focusTrap": true,
"returnFocus": true
}
}
CSS-пресеты (концепт):
css
.backdrop[data-open]{position:fixed; inset:0; background:rgba(0,0,0,.5); backdrop-filter:saturate(80%); opacity:1; transition:opacity. 16s}
.dialog,[data-drawer]{position:fixed; background:var(--bg-elevated); border-radius:12px; box-shadow:var(--elev-4);}
.dialog{inset:auto; left:50%;top:50%;transform:translate(-50%,-50%); max-width:840px; width:min(100% - 32px, var(--dialog-w,680px));}
[data-drawer="right"]{top:0; right:0; height:100%;width:var(--drawer-w,480px); transform:translateX(0)}
.dialog[hidden],.backdrop[hidden]{display:none}
13) Сниппеты поведения
Focus trap + возврат фокуса:js const openBtn = document. getElementById('open');
const dlg = document. querySelector('.dialog');
let prevFocus;
function openDialog() {
prevFocus = document. activeElement;
dlg. hidden = false; document. body. style. overflow = 'hidden';
const focusable = dlg. querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
(focusable[0] dlg). focus();
function onKey(e){
if(e. key==='Escape') return closeDialog();
if(e. key!=='Tab') return;
const first = focusable[0], last = focusable[focusable. length-1];
if(e. shiftKey && document. activeElement===first){ e. preventDefault(); last. focus(); }
else if(!e.shiftKey && document. activeElement===last){ e. preventDefault(); first. focus(); }
}
dlg. addEventListener('keydown', onKey);
dlg. dataset. off = ()=> dlg. removeEventListener('keydown', onKey);
}
function closeDialog() {
dlg. dataset. off && dlg. dataset. off();
dlg. hidden = true; document. body. style. overflow = '';
prevFocus && prevFocus. focus();
}
Sheet с жестом закрытия (мобайл, упрощенно):
js let startY=0, delta=0;
const sheet = document. querySelector('.sheet');
sheet. addEventListener('touchstart', e => startY = e. touches[0].clientY);
sheet. addEventListener('touchmove', e => {
delta = Math. max(0, e. touches[0].clientY - startY);
sheet. style. transform = `translateY(${delta}px)`;
});
sheet. addEventListener('touchend', () => {
if (delta > 120) sheet. classList. remove('open'); else sheet. style. transform = '';
delta = 0;
});
14) Метрики и эксперименты
Open Rate / Completion Rate по модалкам: сколько открыли и завершили действие.
Time-to-Decision: от открытия до клика по Primary.
Dismiss Rate и причины (закрытие по Esc/фону vs «Отмена»).
Error/Retry Rate в busy-сценариях.
A/B: modal vs drawer, текст CTA, порядок полей, «подтверждение» vs «undo».
15) QA-чек-лист
Доступность
- `role="dialog"`, `aria-modal="true"`, правильные `aria-labelledby/-describedby`.
- Focus trap работает; фокус возвращается к триггеру.
- Esc закрывает (если разрешено); Tab цикличен.
- Контраст ≥ AA; не только цвет передает смысл.
Поведение
- TTFF ≤ 100 мс; анимации in 120–180 мс / out 80–140 мс.
- Scroll-lock фона без «прыжка» страницы.
- Unsaved-guard при грязной форме.
- Busy-состояние, корректные Retry/ошибки.
Интерфейс
- Ясный заголовок и один Primary-CTA.
- Крестик/кнопка «Закрыть» доступны.
- Размеры адаптивны; на мобилке — sheet с жестом.
Перформанс
- Порталы/z-index корректны; без «просвечивания».
- Ленивая инициализация; анимируются только transform/opacity.
16) Документация в дизайн-системе
Компоненты: `Modal`, `Drawer/Sheet`, `ConfirmDialog`, `UnsavedGuard`.
Токены: размеры, отступы, тени, анимации, backdrop, focus-ring.
Гайды: «Когда modal vs drawer», шаблоны копирайтинга, рискованные действия (confirm/undo), scroll-lock и portals, reduce-motion.
Do/Don’t-галерея: nested modals (don’t), длинные формы в модалке (don’t), sheet для расширения контекста (do).
Краткое резюме
Модалка — для решений под полным вниманием, drawer — для расширения контекста без разрыва потока. Держите структуру простой, CTA — однозначным, а взаимодействия — предсказуемыми и доступными. Уважайте перформанс, блокируйте фон и возвращайте фокус. В сценариях iGaming это напрямую влияет на доверие: подтверждения ставок, кэшаут, выбор платежного метода и KYC должны быть честными, быстрыми и безопасными.