Универсальный Drawer на Vue: один компонент вместо модалки, сайдбара и bottom sheet

от автора

в
Время чтения: 2 мин.

В интерфейсах постоянно повторяется один и тот же паттерн:
что-то должно появиться поверх страницы — сбоку, снизу или на весь экран.

Обычно это реализуют по-разному:

  • отдельная модалка
  • отдельный sidebar
  • отдельный mobile menu
  • отдельный bottom sheet

В итоге — куча дублирующегося кода.

А можно сделать один универсальный компонент, который покрывает всё это.


💡 Идея

Компонент управляется через v-model и умеет:

  • выезжать с любой стороны (left | right | top | bottom)
  • работать как fullscreen overlay
  • блокировать скролл страницы
  • закрываться по ESC
  • анимироваться через transform
  • рендериться поверх всего через teleport

🧩 Пример использования

<Drawer v-model="opened" side="right">
<div class="p-4">
Контент панели
</div>
</Drawer>

Или, например, mobile bottom sheet:

<Drawer v-model="opened" side="bottom">
<div class="p-4">
Фильтры
</div>
</Drawer>

Fullscreen режим:

<Drawer v-model="opened" fullscreen>
<div class="p-6">
Полноэкранный контент
</div>
</Drawer>

🧠 Что происходит внутри

1. Управление состоянием

const opened = defineModel<boolean>({ default: false })

Компонент полностью контролируется снаружи через v-model.


2. Закрытие по Escape

useEventListener('keydown', e => {
if (e.key === 'Escape' && opened.value) close()
})

Никаких лишних обработчиков — просто глобальный listener.


3. Блокировка скролла

watch(opened, v => document.body.style.overflow = v ? 'hidden' : '')

Когда drawer открыт — фон не скроллится.

Важно, что есть cleanup:

onUnmounted(() => {
document.body.style.overflow = ''
})

4. Teleport — ключевая деталь

<teleport to="body">

Это решает сразу несколько проблем:

  • z-index становится предсказуемым
  • не ломается из-за overflow: hidden у родителей
  • не зависит от layout страницы

5. Анимация через transform

Вся магия — в translate:

props.side === 'left' ? '-translate-x-full'

👉 компонент изначально за пределами экрана
👉 затем анимируется в translate(0)

Почему это важно:

  • нет layout thrashing
  • работает через GPU
  • предсказуемо и быстро

6. Универсальная геометрия

В зависимости от side:

sideповедение
leftвыезжает слева
rightсправа
topсверху
bottomснизу

Размеры подобраны под реальные кейсы:

  • боковые панели → 80vw + max-w-sm
  • верх/низ → 50vh
  • fullscreen → 100vw / 100vh

🔥 Почему это удобно

1. Один компонент — много сценариев

  • mobile menu
  • фильтры
  • корзина
  • настройки
  • модалки

2. Нет дублирования

Вместо:

  • Modal.vue
  • Sidebar.vue
  • BottomSheet.vue

→ один Drawer.vue


3. Простое API

<Drawer v-model="open" side="left" />

И всё.


4. Контроль снаружи

Ты полностью управляешь состоянием —
никакой скрытой логики внутри.


⚠️ Что можно добавить

Если довести до production-уровня:

1. Backdrop (затемнение)

<div class="fixed inset-0 bg-black/40" />

2. Закрытие по клику вне


3. Focus trap (a11y)

Чтобы таб не выходил за пределы панели


4. Swipe закрытие (mobile)


5. Стек модалок

Если их может быть несколько


🧠 Вывод

Это не просто «ещё один компонент».

Это унифицированный слой поверх UI, который:

  • упрощает архитектуру
  • убирает дублирование
  • делает поведение предсказуемым

И главное — его легко расширять под любые сценарии.


Если кратко:

👉 один Drawer может заменить половину overlay-компонентов в проекте


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Сколько будет 5 + 3?