Урок 14. Теория: движение в интерфейсе

📁 Раздел: Состояния и темы ⏱️ Время изучения: ~45 мин 🎯 Сложность: Средняя

⚡ Кратко

Переход описывает, как элемент попадает из одного состояния в другое. Анимируйте только дешёвые свойства — transform и opacity: они не заставляют браузер пересчитывать раскладку. Длительность интерфейсной анимации — 150–300 мс: меньше не заметно, больше раздражает.

Переход = что + сколько + как

Три класса покрывают почти все случаи:

  • transition — анимировать типовой набор свойств (цвет, фон, границу, тень, трансформацию);
  • duration-200 — длительность в миллисекундах;
  • ease-out — кривая ускорения.
<div class="flex flex-wrap gap-3">
  <button class="rounded-md bg-sky-600 px-4 py-2 text-sm text-white">без transition</button>
  <button class="rounded-md bg-sky-600 px-4 py-2 text-sm text-white transition-colors duration-200 hover:bg-sky-800">transition-colors 200мс</button>
  <button class="rounded-md bg-sky-600 px-4 py-2 text-sm text-white transition-colors duration-700 hover:bg-sky-800">duration-700 — слишком долго</button>
</div>

Наведите на все три кнопки: первая переключается мгновенно, вторая — приятно, третья ощущается «залипшей». Это и есть практическая разница между значениями duration.

Переход — как закрывание двери с доводчиком. Без доводчика дверь хлопает: пользователь не понимает, что произошло. С правильным доводчиком движение читается. С слишком тугим — вы стоите и ждёте, пока дверь закроется, вместо того чтобы идти дальше.

Кривые ускорения

КлассПоведениеКогда
ease-linearравномернобесконечные анимации: спиннер, прогресс
ease-inмедленно → быстроисчезновение, уход за экран
ease-outбыстро → медленнопоявление: самый частый выбор
ease-in-outплавно с обоих концовперемещение внутри экрана
Правило по умолчанию: появление — ease-out, исчезновение — ease-in. Так объект «влетает» энергично и «уходит» мягко — в жизни предметы ведут себя похоже.

Трансформации: движение без пересчёта раскладки

transform двигает, поворачивает и масштабирует элемент, не трогая соседей — браузер не пересчитывает раскладку, а значит, анимация не «дёргается».

<div class="flex flex-wrap items-center gap-6">
  <div class="size-16 rounded-lg bg-indigo-500 transition-transform duration-200 hover:scale-110"></div>
  <div class="size-16 rounded-lg bg-emerald-500 transition-transform duration-200 hover:-translate-y-2"></div>
  <div class="size-16 rounded-lg bg-amber-500 transition-transform duration-300 hover:rotate-12"></div>
  <div class="size-16 rounded-lg bg-rose-500 transition-transform duration-200 hover:skew-x-6"></div>
</div>

Слева направо: hover:scale-110, hover:-translate-y-2, hover:rotate-12, hover:skew-x-6. Точку отсчёта задаёт origin-*: например, origin-bottom для «вырастающего» элемента.

Готовые анимации

КлассЧто делаетГде применяют
animate-spinбесконечное вращениеиндикатор загрузки
animate-pulseмигание прозрачностьюскелетоны
animate-pingрасходящееся кольцопривлечь внимание к точке
animate-bounceподпрыгиваниеуказатель «листайте вниз»
animate-noneотключитьпереопределение
↓
<div class="flex flex-wrap items-center gap-6">
  <span class="inline-block size-6 animate-spin rounded-full border-2 border-slate-300 border-t-slate-900"></span>
  <div class="w-40 space-y-2">
    <div class="h-3 animate-pulse rounded bg-slate-200"></div>
    <div class="h-3 w-2/3 animate-pulse rounded bg-slate-200"></div>
  </div>
  <span class="relative flex size-3">
    <span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75"></span>
    <span class="relative inline-flex size-3 rounded-full bg-emerald-500"></span>
  </span>
  <span class="animate-bounce text-2xl">↓</span>
</div>

Своя анимация

Собственные ключевые кадры описываются обычным CSS, а имя попадает в тему — после этого доступен класс animate-fade-up:

/* src/index.css */
@import "tailwindcss";

@theme {
  --animate-fade-up: fade-up 0.25s ease-out;
}

@keyframes fade-up {
  from { opacity: 0; transform: translateY(0.5rem); }
  to   { opacity: 1; transform: translateY(0); }
}

Этот текст появился с анимацией fade-up

<div class="rounded-lg border border-slate-200 bg-white p-4">
  <p class="animate-fade-up text-sm text-slate-700">Этот текст появился с анимацией fade-up</p>
</div>

Появление элементов: starting

Долгое время анимировать появление можно было только через JavaScript. Теперь есть @starting-style, а в Tailwind — вариант starting:: он задаёт значения, с которых элемент начинает существование.

<div class="opacity-100 transition-opacity duration-300 starting:opacity-0">
  Появляюсь плавно
</div>
⚠️ Проверить по документации: поддержка @starting-style зависит от браузера. Для критичной анимации предусмотрите запасной вариант.

Уважайте настройку «уменьшить движение»

Часть людей отключает анимации из-за вестибулярных нарушений — для них движение вызывает физическое недомогание. Система сообщает об этом через prefers-reduced-motion, а Tailwind даёт варианты motion-safe: и motion-reduce:.

<!-- анимация только для тех, кто её не отключал -->
<div class="motion-safe:animate-bounce">↓</div>

<!-- или наоборот: выключить движение для тех, кто просил -->
<div class="animate-pulse motion-reduce:animate-none">…</div>
Проверить у себя: Windows — «Параметры → Специальные возможности → Визуальные эффекты → Эффекты анимации»; macOS — «Универсальный доступ → Дисплей → Уменьшить движение». Включите и пройдитесь по своему интерфейсу.

Производительность

  • Дёшево: transform, opacity — обрабатывает видеокарта, раскладка не пересчитывается.
  • Дорого: width, height, top, margin — каждый кадр браузер пересчитывает раскладку.
  • will-change-transform — подсказка браузеру; ставьте её точечно и не оставляйте навсегда.
<!-- ❌ дёргается: анимируется ширина -->
<div class="w-32 transition-all hover:w-64">…</div>

<!-- ✅ плавно: анимируется масштаб -->
<div class="w-32 transition-transform hover:scale-x-150 origin-left">…</div>