Анимации и адаптивная вёрстка: transition, keyframes, медиазапросы
Анимации и адаптив — две дисциплины, которые превращают «статичную картинку» в интерфейс. Плохая анимация не просто «некрасиво» — она буквально дёргает интерфейс на слабых устройствах, вызывая пропуски кадров и ощущение «тормознутости». Плохая адаптивность — это когда на телефоне страница требует зума, а на 4K-мониторе растягивается в тонкую ленту. Обе проблемы решаются пониманием одной и той же механики: что браузер может анимировать дёшево и от чего зависит размер элементов.
В краткой версии ты добавлял transition: all 0.3s и один медиазапрос «под мобильные». Здесь разбираем: почему all — антипаттерн, как устроены кубики Безье timing-функций, какие свойства проходят через compositor без перерисовки, почему анимации должны выключаться при prefers-reduced-motion, и как строить адаптив от контента, а не от устройств.
Transition: от состояния к состоянию
Заголовок раздела «Transition: от состояния к состоянию»transition описывает, как элемент перетекает между двумя значениями свойства при изменении (hover, класс, JS-стиль):
.btn { transition: background-color 0.2s ease-in-out, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1); /* подсвойства: property | duration | timing-function | delay */}| Подсвойство | Что задаёт | Пример |
|---|---|---|
transition-property |
что анимируем (список, НЕ all) |
opacity, transform |
transition-duration |
длительность | 0.2s, 200ms |
transition-timing-function |
кривая ускорения | ease, cubic-bezier(…) |
transition-delay |
задержка старта | 0.1s |
Кубики Безье: читаем timing-function
Заголовок раздела «Кубики Безье: читаем timing-function»cubic-bezier(x1, y1, x2, y2) описывает кривую в координатах «время → прогресс». Ось X — время (всегда 0→1), ось Y — доля завершения (может выходить за 0..1 — отсюда «пружинящие» эффекты). Кривые удобно проектировать визуально — в интерактивном редакторе cubic-bezier.com:
линейная: ease (по умолчанию): cubic-bezier(.34,1.56,.64,1) «пружина»:
y│ y│ y│ ╭─▶ прогресс > 11├──────── 1├──────── 1├───────╯ (перелёт и возврат) │ ╱ │ ╭── │ ╭─╯ │ ╱ │ ╱ │ ╱ │ ╱ │ ╱ │╱0├╱─────── x 0├╱─────── x 0├─────── x 0 1 время 0 1 0 1ease= быстро старт, плавный финиш — универсален, но «вялый» для микровзаимодействий.ease-out— быстрый старт, затухание: стандарт для появления элементов (карточки, тултипы) — интерфейс реагирует мгновенно.ease-in— медленный старт, быстрый финиш: только для исчезновения, иначе ощущается «залипанием».cubic-bezier(0.34, 1.56, 0.64, 1)— лёгкий перелёт через цель и возврат: живая «пружина» для кнопок и бейджей.
delay используется и для стаггер-эффектов (каскад появления списка: --i индекс × 30ms), и как ловушка: большая задержка делает интерфейс «ленивым», а на тач-устройствах hover-переходы с delay вообще не чувствуются.
Что анимировать: дорожная карта кадра
Заголовок раздела «Что анимировать: дорожная карта кадра»Браузер рисует страницу в три стадии:
JS/CSS-изменение │ ▼┌─────────────┐ layout (reflow): пересчёт геометрии — ВСЁ: width, height,│ STYLE │ top, margin, font-size, добавление узла…└─────────────┘ │ │ ▼┌─────────────┐ paint (repaint): перерисовка пикселей отдельных слоёв:│ LAYOUT │ background, color, border, box-shadow…└─────────────┘ │ │ ▼┌─────────────┐ composite: сборка готовых слоёв на GPU:│ PAINT │ transform, opacity — БЕЗ layout/paint└─────────────┘ │ ▼ ЭкранЦена стадий растёт экспоненциально: composite почти бесплатен, layout на большой странице — десятки миллисекунд. Отсюда золотое правило: анимируй transform и opacity; всё, что вызывает layout (width, height, top, left, margin, font-size), меняй мгновенно или через FLIP-приёмы. Подтверждения и инструменты замера — в web.dev: Animations and performance.
/* плохо: top/left гоняют layout на каждом кадре */.ball { transition: top 0.5s, left 0.5s; }
/* хорошо: transform улетает на compositor */.ball { transition: transform 0.5s; }.ball--moved { transform: translate(200px, 100px); }width/height иногда нужны анимировать (аккордеон). Компромисс — max-height от 0 до большого значения или современный interpolate-size: allow-keywords (поддержка ещё ограничена, проверяй).
@keyframes и animation
Заголовок раздела «@keyframes и animation»Transition — между двумя состояниями; @keyframes — полный сценарий:
@keyframes pulse { 0% { transform: scale(1); opacity: 1 } 50% { transform: scale(1.05); opacity: 0.8 } 100% { transform: scale(1); opacity: 1 }}
.spinner { animation: pulse 2s ease-in-out infinite;}| Подсвойство | Значение | Примечание |
|---|---|---|
animation-name |
имя @keyframes | — |
animation-duration |
длительность цикла | — |
animation-timing-function |
кривая | внутри keyframes можно менять по кадрам |
animation-delay |
старт | отрицательный — старт «из середины» |
animation-iteration-count |
1 / infinite / 3.5 |
— |
animation-direction |
normal / reverse / alternate / alternate-reverse |
alternate: туда-обратно без рывка |
animation-fill-mode |
none / forwards / backwards / both |
что до/после анимации |
animation-play-state |
running / paused |
пауза по hover/JS |
fill-mode — самое недопонятое:
backwards: до старта (в т.ч. в период delay) элемент уже в кадре0%.forwards: после финала элемент остаётся в кадре100%(иначе мгновенно схлопнется обратно).
Отдельные анимации на hover должны быть короткими и обратимыми: при уходе мыши transition доиграет, а прерванная animation — перезапустится с нуля. Для hover-подсветок используй transition, keyframes оставь для автономных сценариев (лоадеры, привлечение внимания).
will-change: ускоритель с побочкой
Заголовок раздела «will-change: ускоритель с побочкой»will-change: transform говорит браузеру: «держи элемент на отдельном слое заранее». Это убирает стоимость СОЗДАНИЯ слоя в первом кадре анимации. Но:
- каждый слой — память GPU;
- слои слишком мелких элементов дороже создания, чем выигрыш;
- постоянный
will-changeна сотнях карточек — прямой путь к OOM на слабых устройствах.
Правило: will-change ставится точечно и временно (класс .is-animating на время анимации через JS), а не как постоянный атрибут раскладки.
prefers-reduced-motion: анимации — не для всех
Заголовок раздела «prefers-reduced-motion: анимации — не для всех»Для части пользователей (вестибулярные расстройства, мигрени) параллакс, «пружины» и бесконечные пульсации вызывают тошноту и головную боль. ОС имеет переключатель «уменьшить движение» — и браузер отдаёт его в медиазапрос prefers-reduced-motion. Это обязательная доступность, а не опция:
.card { transition: transform 0.3s, box-shadow 0.3s; }.card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgb(0 0 0 / 0.15); }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }}Декоративную анимацию выключаем полностью; функциональную (появление модалки) — заменяем на мгновенную смену состояния или простое затухание opacity. Каскадные сбросы через * — нормальная практика, их легитимность обоснована именно доступностью.
Медиазапросы: синтаксис и логика
Заголовок раздела «Медиазапросы: синтаксис и логика»/* базовый синтаксис */@media (min-width: 768px) { … } /* вьюпорт ≥ 768px */@media (min-width: 768px) and (max-width: 1023.98px) { … } /* диапазон */@media (orientation: landscape) { … }@media (hover: hover) and (pointer: fine) { … } /* устройство с мышью */
/* операторы */and — оба условия, — ИЛИ: @media (max-width: 600px), (orientation: portrait)only — старый хак скрытия от IE: @media only screen and (…)not — инверсия (нельзя с not использовать only)Mobile-first против desktop-first
Заголовок раздела «Mobile-first против desktop-first»Подход определяет, от какого конца «растёт» раскладка:
MOBILE-FIRST (современный стандарт) DESKTOP-FIRST (легаси-подход)
база: мобильная (1 колонка) база: десктоп (4 колонки) │ │ ▼ min-width: 640px ▼ max-width: 1024px планшет (2 колонки) планшет (2 колонки) │ │ ▼ min-width: 1024px ▼ max-width: 640px десктоп (4 колонки) мобильная (1 колонка)
@media (min-width: 640px) { } @media (max-width: 1024px) { }@media (min-width: 1024px) { } @media (max-width: 640px) { }Почему mobile-first выиграл: мобильный трафик больше; «добавлять» сложность через min-width проще, чем «вычитать»; производительность: девайс получает только нужные стили без перекрытий. Грабля min-width-стиля: ширина ВКЛЮЧИТЕЛЬНА — (min-width: 768px) срабатывает ровно на 768, поэтому диапазоны пишут через max-width: 1023.98px (пол-пикселя против наложения). Полный синтаксис медиазапросов с операторами — в MDN: Использование медиазапросов.
Breakpoints: от контента, не от устройств
Заголовок раздела «Breakpoints: от контента, не от устройств»«Телефон 375, планшет 768, десктоп 1024» — миф: устройств тысячи, и новый планшет выйдет раньше, чем ты пересоберёшь CSS. Правильный принцип: breakpoint ставится там, где раскладка ЛОМАЕТСЯ (строка текста выходит за 90 символов, карточки сжались до нечитаемости). Метод: уменьшай окно, следи, где верстка «хрустнула», — это и есть точка.
Тем не менее, типовые стартовые точки (Bootstrap-like), которые закрывают 95% кейсов:
| Название | Диапазон | Что там происходит |
|---|---|---|
| xs | < 576px | телефоны, одноколоночный поток |
| sm | ≥ 576px | крупные телефоны/мелкие планшеты |
| md | ≥ 768px | планшеты, 2 колонки |
| lg | ≥ 992px | лэптопы, полная раскладка |
| xl | ≥ 1200px | десктопы, широкие сетки |
| xxl | ≥ 1400px | большие мониторы |
Ещё медиа-фичи
Заголовок раздела «Ещё медиа-фичи»@media (prefers-color-scheme: dark) { … } /* тёмная тема ОС */@media (prefers-contrast: more) { … } /* высококонтрастный режим */@media (prefers-reduced-motion: reduce) { … } /* см. выше */Тёмная тема — не инверсия: используй кастомные свойства из главы fundamentals и переключай значения, сохраняя акценты и читаемость. prefers-contrast — для пользователей с ослабленным зрением: усиль границы и убери полупрозрачность.
Fluid-типографика: clamp() вместо breakpoint’ов
Заголовок раздела «Fluid-типографика: clamp() вместо breakpoint’ов»body { font-size: clamp(1rem, 0.6rem + 0.8vw, 1.25rem); /* минимум предпочтение максимум */}Как это работает: предпочтение — линейная функция от vw (0.6rem + 0.8vw), clamp обрезает её снизу и сверху. Результат: плавное масштабирование между двумя «виртуальными breakpoint’ами» без единого медиазапроса. Формула предпочтения из двух точек (x1, y1) → (x2, y2):
slope = (y2 − y1) / (x2 − x1) в vw: slope × 100pref = (y1 − slope × x1) в rem + slope × 100 vwНапример, 1rem при 400px → 1.25rem при 1200px: slope = 0.25/800 = 0.0003125 → clamp(1rem, 0.875rem + 0.3125vw, 1.25rem). Генераторы (utopia.fyi) считают за тебя; важно понимать механику, чтобы не копипастить магию.
Container queries: медиазапросы для компонентов
Заголовок раздела «Container queries: медиазапросы для компонентов»Медиазапрос смотрит на ВЬЮПОРТ. Но карточка в сайдбаре и та же карточка в основной колонке должны выглядеть по-разному при ОДНОМ вьюпорте. Container query смотрит на размер КОНТЕЙНЕРА компонента:
.cards { container-type: inline-size; /* контейнер измеряется по ширине */ container-name: cards;}
.card__meta { display: none; /* на узких — прячем мету */}
@container cards (min-width: 400px) { .card__meta { display: block } .card { flex-direction: row } /* и перестраиваем карточку */}Один и тот же компонент адаптируется САМ — независимо от страницы, на которой оказался. Это конец эпохи «модификаторов .card–in-sidebar». Поддержка: все современные браузеры с 2023. Ограничение: нельзя стилизовать сам контейнер по его же query — только потомков; каскад отрабатывает как обычно (container queries живут внутри того же каскада). Синтаксис и примеры — в MDN: Container queries.
Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»- Анимация width/height/top/left. Главный источник «дёрганого» интерфейса. Переводи на transform/opacity; для раскрытия блоков — max-height или grid-rows трюки.
transition: all. Анимирует случайные свойства, мешает браузеру планировать слои, ломает perf-аудиты. Перечисляй явно.overflow-x: hiddenна body «от прыжков». Часто маскирует переполнение от абсолютных элементов; лучше найти и починить источник вылезающего контента.- Медиазапросы «под устройства» и 20 breakpoint’ов. Раскладка ломается в неожиданных местах между 361 и 374px — ставь точки по факту поломки, не по каталогам девайсов.
hover-стили как единственный индикатор интерактивности. На тач-устройствах hover либо «залипает» после тапа, либо не срабатывает. Стилизуй и:focus-visible, и@media (hover: hover)-варианты.- Отсутствие prefers-reduced-motion. Бесконечные анимации без этой обработки — прямой риск по accessibility-аудиту и реальной боли пользователей.
will-changeна всём подряд. Постоянные слои на сотнях элементов = память GPU + иногда ВРЕД производительности. Точечно, на время анимации.- Мгновенные отключения анимаций через
animation: noneв reduced-motion. Лучше оставить функциональные микропереходы (opacity 0.01ms) — резкие «прыжки» состояния тоже дезориентируют.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»- Почему transform/opacity предпочтительнее width/top для анимаций? Transform и opacity обрабатываются на этапе composite на GPU — без layout и paint. Width/top/left вызывают reflow (пересчёт геометрии) и repaint на каждом кадре, что на слабых устройствах даёт пропуски кадров.
- Что такое кубик Безье timing-function и что означают четыре числа? Кривая ускорения в координатах «время → прогресс»: точки управления (x1,y1) и (x2,y2), x — время (0..1), y — прогресс (может > 1 для «пружины»). ease-out = быстрый старт, плавный финиш.
- Чем transition отличается от animation? Transition — перетекание между двумя состояниями по триггеру (hover/класс), обратимо. Animation — полный сценарий @keyframes, с циклами, направлением, fill-mode; может работать автономно.
- Что делает animation-fill-mode: forwards и backwards? Forwards: после завершения элемент остаётся в состоянии последнего кадра. Backwards: до старта (включая период delay) элемент уже находится в состоянии первого кадра.
- Зачем нужен prefers-reduced-motion и как его обрабатывать? Отражает настройку ОС «уменьшить движение» для пользователей с вестибулярными расстройствами. Обязательная обработка: отключать/упрощать декоративные анимации, сохранять функциональные микропереходы.
- Mobile-first против desktop-first — в чём разница и что выбрать? Mobile-first: база под малые экраны, усложнение через min-width. Desktop-first: наоборот, max-width. Современный стандарт — mobile-first: больший трафик, аддитивная сложность, лучше производительность.
- Как работает clamp() для fluid-типографики?
clamp(min, preferred, max): preferred — линейная функция vw+rem, clamp обрезает по границам. Получаем плавное масштабирование между двумя контрольными ширинами без медиазапросов. - Чем container query отличается от медиазапроса? Медиазапрос реагирует на вьюпорт, container query — на размер контейнера компонента. Позволяет одному и тому же компоненту адаптироваться к месту на странице (сайдбар/основная колонка) независимо от устройства.
Практика
Заголовок раздела «Практика»- Кривые ускорения. Кнопка: hover — подъём на 4px и тень за 200ms с «пружиной» cubic-bezier(0.34, 1.56, 0.64, 1); active — плавное «вжатие». Критерий: перелёт на hover чувствуется, на active — нет; всё на transform/box-shadow.
- Дорогая против дешёвой анимация. Сделай бокс, летящий по диагонали дважды: через
top/leftи черезtransform. Замерь в DevTools → Performance разницу в количестве layout-событий. Критерий: скриншот flame graph’а, где виден reflow в первом случае. - Reduced motion. Страница с параллакс-фоном и hover-карточками. Добавь полный сброс анимаций при
prefers-reduced-motion: reduceи проверь эмуляцией в DevTools → Rendering. Критерий: при включённой эмуляции ни одно свойство не анимируется. - Fluid-типографика. Заголовок 1.5rem при 400px → 3rem при 1400px через clamp(). Выведи формулу preferred сам, сверь с utopia.fyi. Критерий: нет медиазапросов, размер строго в границах.
- Container query. Карточка, которая в узком контейнере показывает только заголовок, а от 400px — горизонтальный layout с описанием. Помести две такие карточки: в сайдбар 280px и в main 700px. Критерий: обе выглядят корректно при одном вьюпорте, без классов-модификаторов.
Что почитать
Заголовок раздела «Что почитать»- MDN: transition и MDN: animation — подсвойства, синтаксис, примеры.
- MDN: Стилизация анимаций — @keyframes в деталях.
- web.dev: Animations and performance — высокопроизводительные анимации и инструменты замера.
- cubic-bezier.com — интерактивный редактор кубиков Безье с наглядной кривой.
- MDN: Медиазапросы и MDN: Container queries — полный синтаксис обоих.
- Utopia: fluid type scale calculator — генератор clamp()-формул с объяснением математики.