В интерфейсах постоянно приходится переключать состояние:
- форма → “успешно отправлено”
- карточка → подробности
- заглушка → реальный контент
- компактный блок → расширенный
Обычно это делают через v-if. И обычно это выглядит… плохо.
Резкий скачок, дергается высота, ломается восприятие.
В этой статье разберём простой, но мощный паттерн — анимированный переключатель контента, который делает интерфейс заметно приятнее.
🧩 Сам компонент
<script setup lang="ts">
defineProps({
showPrimary: {
type: Boolean,
required: true,
},
})
</script>
<template>
<div class="toggle-grid">
<Transition name="expand">
<div v-if="showPrimary" class="toggle-cell">
<slot name="primary" />
</div>
<div v-else class="toggle-cell">
<slot name="secondary" />
</div>
</Transition>
</div>
</template>
<style>
.toggle-grid {
display: grid;
grid-template-areas: 'stack';
}
.toggle-cell {
grid-area: stack;
}
.expand-enter-active,
.expand-leave-active {
transition:
max-height 0.3s ease,
opacity 0.2s ease 0.1s;
}
.expand-enter-from,
.expand-leave-to {
max-height: 0;
opacity: 0;
}
.expand-enter-to,
.expand-leave-from {
max-height: 1000px;
opacity: 1;
}
</style>🎯 Какую задачу он решает
Этот компонент делает одну вещь:
плавно заменяет один блок другим без скачков интерфейса
То есть вместо:
- мгновенного исчезновения
- пересчёта layout
- дерганий
мы получаем:
- мягкое схлопывание
- плавное появление
- стабильный UI
🚫 Почему v-if — это проблема
Базовый подход:
<div v-if="showPrimary">...</div>
<div v-else>...</div>
Что происходит:
- DOM меняется мгновенно
- высота контейнера резко меняется
- пользователь “теряет” контекст
Особенно заметно:
- в формах
- в карточках
- в списках
⚙️ Как это работает
1. Управление через один флаг
showPrimary: boolean
true→ основной контентfalse→ альтернативный
Просто и предсказуемо.
2. Слоты вместо жёсткой логики
<slot name="primary" />
<slot name="secondary" />
Компонент:
- не знает, что внутри
- работает с любым UI
👉 это делает его переиспользуемым везде
3. Grid-стек вместо absolute
.toggle-grid {
display: grid;
grid-template-areas: 'stack';
}.toggle-cell {
grid-area: stack;
}
Оба состояния:
- занимают одну и ту же область
- не “толкают” layout
💡 Это ключ к отсутствию скачков.
4. Анимация через max-height
.expand-enter-from,
.expand-leave-to {
max-height: 0;
opacity: 0;
}.expand-enter-to,
.expand-leave-from {
max-height: 1000px;
opacity: 1;
}
Что происходит:
- блок “схлопывается” →
max-height: 0 - затем новый “разворачивается”
- плюс лёгкий fade
👉 ощущается как естественное раскрытие
🧠 Почему именно max-height
Проблема:
height: auto;
❌ нельзя анимировать
Решение:
max-height: 1000px;
✔ работает как “псевдо-auto”
Да, это хак. Но это стандарт индустрии.
💡 Где это реально полезно
1. Формы
<ToggleExpand :showPrimary="!submitted">
<template #primary>
<Form />
</template>
<template #secondary>
<SuccessMessage />
</template>
</ToggleExpand>
2. Карточки
- краткое описание → подробности
3. Загрузка данных
- skeleton → контент
4. Фильтры / настройки
- свернуто → раскрыто
⚠️ Ограничения (важно)
1. Ограничение по высоте
max-height: 1000px;
Если контент выше → обрежется
👉 решение:
- увеличить значение
- или делать JS-измерение
2. Не подходит для сложных переходов
Это:
- не crossfade
- не одновременная анимация
А именно:
👉 “один ушёл → другой пришёл”
3. Только два состояния
Если нужно:
- 3+ состояний
- сложная логика
лучше:
- динамический компонент
- state machine
🔥 Почему это крутой паттерн
Потому что он:
- простой (20 строк)
- переиспользуемый
- сильно улучшает UX
- не требует JS-магии
И главное:
он убирает ощущение “дёрганого интерфейса”
🧪 Как можно улучшить
Если хочется глубже:
- убрать
max-height→ считать высоту через JS - добавить
mode="out-in"в Transition - сделать crossfade (через absolute)
- добавить easing под бренд
📌 Итог
Это один из тех компонентов, которые:
- не выглядят важными
- но сильно влияют на восприятие продукта
Ты просто меняешь v-if на этот паттерн —
и интерфейс становится ощутимо приятнее.


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