Урок 21. Старый vs Новый: самописный модал и примитив

📁 Раздел: shadcn/ui ⏱️ Время изучения: ~55 мин 🎯 Сложность: Продвинутая

⚡ Кратко

Самописный модал — это фокус-ловушка, 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.OverlayDialog.Backdrop
ОкноDialog.ContentDialog.Popup
Состояниеdata-[state=open]data-open
Закрытиеdata-[state=closed]data-closed
ПолиморфизмasChildrender
Пункт меню под курсоромdata-[highlighted]data-highlighted
Пакет анимацийtailwindcss-animatetw-animate-css
Практический признак: если в примере из интернета встречается data-[state=open]:animate-in, а в вашем компоненте data-open:animate-in — это разные поколения. Ориентируйтесь на файл в своём проекте.

Что осталось неизменным

  • структура: триггер → портал → подложка → содержимое;
  • обязательный заголовок для доступности;
  • анимации, привязанные к состоянию, а не к таймерам;
  • композиция из мелких частей вместо пропсов-флагов.