В интерфейсах постоянно повторяется один и тот же паттерн:
что-то должно появиться поверх страницы — сбоку, снизу или на весь экран.
Обычно это реализуют по-разному:
- отдельная модалка
- отдельный 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.vueSidebar.vueBottomSheet.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-компонентов в проекте


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