Урок 21. Старый vs Новый: самописный модал и примитив
⚡ Кратко
Самописный модал — это фокус-ловушка, Esc, блокировка прокрутки и ARIA, написанные вручную. Плюс к этому в самом shadcn/ui поменялись имена частей и состояний при переходе на Base UI.
Модальное окно своими руками
❌ «Тридцать строк» — и десяток проблем
function Modal({ open, onClose, children }) {
if (!open) return null
return (
<div className="fixed inset-0 z-50 bg-black/50" onClick={onClose}>
<div className="mx-auto mt-24 max-w-sm rounded-xl bg-white p-4"
onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
)
}
Чего здесь нет: фокус не переходит внутрь, Tab уходит
на страницу под окном, Esc не работает, страница
прокручивается, скринридер не знает, что открыт диалог, после закрытия
фокус теряется, окно обрежется, если родитель с
overflow-hidden.
✅ Примитив закрывает всё это
<Dialog>
<DialogTrigger render={<Button>Открыть</Button>} />
<DialogContent>
<DialogTitle>Заголовок</DialogTitle>
…
</DialogContent>
</Dialog>
Radix → Base UI: что переименовалось
| Что | Radix (старые версии) | Base UI (актуальные) |
|---|---|---|
| Подложка | Dialog.Overlay | Dialog.Backdrop |
| Окно | Dialog.Content | Dialog.Popup |
| Состояние | data-[state=open] | data-open |
| Закрытие | data-[state=closed] | data-closed |
| Полиморфизм | asChild | render |
| Пункт меню под курсором | data-[highlighted] | data-highlighted |
| Пакет анимаций | tailwindcss-animate | tw-animate-css |
Практический признак: если в примере из интернета
встречается
data-[state=open]:animate-in, а в вашем компоненте
data-open:animate-in — это разные поколения. Ориентируйтесь
на файл в своём проекте.Что осталось неизменным
- структура: триггер → портал → подложка → содержимое;
- обязательный заголовок для доступности;
- анимации, привязанные к состоянию, а не к таймерам;
- композиция из мелких частей вместо пропсов-флагов.