HTML & CSS

CSS Анімації та Переходи

Глибоке вивчення CSS transition та @keyframes animation. Функції часу (cubic-bezier), transform, GPU-прискорення, will-change, prefers-reduced-motion. Практика: hover effects, loading spinner, fade-in, modal анімації.

CSS Анімації та Переходи

Різниця між хорошим і чудовим UX — 200 мілісекунд

Відкрийте сайт Apple, Stripe чи Linear і поводьте мишею по кнопках, картках, навігації. Нічого не відбувається "різко" — все переходить плавно: кнопки "наливаються" кольором при наведенні, картки злегка підіймаються, елементи з'являються з легким ковзанням. Ці переходи настільки природні, що ви майже їх не помічаєте. Але якщо їх прибрати — інтерфейс одразу стає "пластиковим", механічним.

Це і є мистецтво CSS-анімацій: добрі анімації не кричать "подивіться на мене!", вони тихо роблять взаємодію приємнішою. Погані анімації — навпаки, заважають, відволікають, дратують.

У цій статті ми розберемо дві системи анімування в CSS: transition для простих станів і @keyframes + animation для складних сценаріїв. Але перш ніж писати код — зрозуміємо, чому анімації взагалі потрібні і як браузер їх виконує під капотом.


Навіщо анімації в інтерфейсах?

Перш ніж писати будь-який анімаційний код, варто зрозуміти психологічну та функціональну роль анімацій у UI. Це не декоративна надмірність — це комунікація.

Анімації як мову системи

Людське зорове сприйняття еволюційно заточено на рух. Рух означає щось важливе: небезпека, їжа, інша істота. У цифровому інтерфейсі цей механізм можна використати на користь користувача:

  • Показати взаємозв'язок. Елемент, що "збирається" в іконку кошика, показує, що товар доданий і пов'язаний із кошиком. Без анімації — це просто зміна числа.
  • Дати зворотний зв'язок. Кнопка, що "пружинить" при кліку, підтверджує: "так, я вас почула". Без неї — система здається задумливою або зламаною.
  • Орієнтувати в просторі. Бічна панель, що виїжджає зліва, підказує: коли вона закриється — вона "поїде" назад ліворуч. Якщо б вона просто зникала — користувач губився б.
  • Привернути увагу до важливого. Помилка, що м'яко "підстрибує", привертає погляд без агресивного блимання.
  • Заповнити час очікування. Spinner або skeleton-екран під час завантаження знижує суб'єктивне відчуття часу очікування.

Правила хорошої анімації

Перш ніж робити анімацію — поставте собі три питання:

  1. Чи вона комунікує щось важливе? Якщо відповідь "ні, просто красиво" — ймовірно, анімацію краще прибрати або спростити.
  2. Чи вона не відволікає від задачі? Анімація фону, що постійно рухається, може бути чудовою на лендингу і катастрофою у робочому дашборді.
  3. Чи вона доступна? Частина користувачів має вестибулярні розлади, і надмірний рух викликає у них фізичний дискомфорт. Ми обов'язково розглянемо prefers-reduced-motion.

Як браузер рендерить сторінку: конвеєр рендеру

Щоб розуміти, які CSS-властивості безпечно анімувати, а які — ні, потрібно знати, як браузер малює сторінку. Цей процес називається rendering pipeline (конвеєр рендеру) і складається з кількох кроків:

Loading diagram...
flowchart LR
    JS["JavaScript"] --> Style["Style\nОбчислення стилів"]
    Style --> Layout["Layout\nРозрахунок розмірів\nта позицій"]
    Layout --> Paint["Paint\nМалювання пікселів"]
    Paint --> Composite["Composite\nСкладання шарів"]
    style JS fill:#f59e0b,color:#fff
    style Layout fill:#ef4444,color:#fff
    style Paint fill:#f97316,color:#fff
    style Composite fill:#10b981,color:#fff
    style Style fill:#6366f1,color:#fff

Кожен крок конвеєру дорогий, але вони не однаково дорогі:

  • Layout (перекомпонування) — найдорожчий. Якщо змінюється width, height, margin, padding, top, left — браузер мусить перерахувати позиції всіх залежних елементів на сторінці. Це може торкнутися сотень вузлів DOM.
  • Paint (перемальовування) — середньої вартості. Якщо змінюється color, background, border — браузер перемальовує пікселі, але не перераховує геометрію.
  • Composite (складання) — найдешевший. Складання вже намальованих шарів є чисто GPU-операцією і відбувається дуже швидко.

Висновок для анімацій: анімуйте тільки властивості, які торкаються тільки Composite-кроку. Таких лише дві: transform та opacity. Все інше спричиняє Layout або Paint, і при 60 fps (16.7мс на кадр) це може помітно гальмувати.

Анімувати width, height, margin, top, left — погана ідея. Навіть якщо на вашому потужному ноутбуці це виглядає плавно, на мобільному пристрої або на слабшому ПК це спричинить "дрижання" (jank). Замінюйте такі анімації на transform: scale() та transform: translate() відповідно.

GPU-прискорені властивості

Браузер автоматично переносить елемент на окремий GPU-шар (compositing layer), якщо він анімується через transform або opacity. Це дозволяє GPU маніпулювати шаром незалежно від CPU і забезпечує стабільні 60 fps.

ВластивістьКрок конвеєруБезпечна для анімації?
transformComposite✅ GPU-прискорена
opacityComposite✅ GPU-прискорена
filterPaint + Composite⚠️ Може бути важкою
color, background-colorPaint⚠️ Прийнятно
width, heightLayout❌ Не рекомендовано
margin, paddingLayout❌ Не рекомендовано
top, leftLayout❌ Замінюйте на translate

transition — плавний перехід між станами

transition (перехід) — найпростіший і найбільш уживаний інструмент анімації в CSS. Він описує, як елемент має плавно перейти від одного CSS-стану до іншого.

Уявіть, що у вас є кнопка. В нормальному стані вона синя. При наведенні (hover) — темно-синя. Без transition зміна відбувається миттєво: синій → темно-синій, похибки немає. З transition браузер інтерполює значення між двома станами за вказаний час:

.btn {
    background-color: #6366f1;
    transition: background-color 0.3s ease;
}

.btn:hover {
    background-color: #4f46e5;
}

Тут ми говоримо браузеру: "коли background-color змінюється — роби це за 0.3 секунди з функцією часу ease". Браузер сам розраховує всі проміжні значення між #6366f1 та #4f46e5.

Синтаксис transition

Властивість transition — це скорочення для чотирьох окремих властивостей:

.element {
    /* transition: властивість тривалість функція-часу затримка */
    transition: background-color 0.3s ease 0s;

    /* Або окремо: */
    transition-property: background-color;
    transition-duration: 0.3s;
    transition-timing-function: ease;
    transition-delay: 0s;
}

Кожен параметр грає свою роль:

  • transition-property — яку CSS-властивість анімувати. Можна вказати all (всі змінні властивості), але це погана практика: transition: all 0.3s ease може ненавмисно анімувати не ті властивості і спричинить Layout-тригери.
  • transition-duration — тривалість у секундах (s) або мілісекундах (ms). 0.2s = 200ms.
  • transition-timing-function — функція часу (крива прискорення). Детально розглянемо нижче.
  • transition-delay — затримка перед початком переходу. Корисна для послідовних анімацій.

Кілька переходів одночасно

.card {
    /* Різні переходи для різних властивостей — через кому */
    transition:
        transform 0.3s ease,
        box-shadow 0.3s ease,
        opacity 0.2s linear;
}

.card:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
    opacity: 0.95;
}

Кожна властивість може мати власну тривалість і функцію часу — це дозволяє тонко контролювати відчуття від взаємодії.

Preview
×
🔒localhost:3000

Наведіть курсор на кожну кнопку і відчуйте різницю у характері переходу — навіть у межах простого hover ефекту є багатий простір для нюансів.


Функції часу (Timing Functions) — душа анімації

Якщо тривалість — це "як довго", то функція часу — це "з яким характером". Дві анімації з однаковою тривалістю, але різними функціями часу — абсолютно різні за відчуттям.

Фізична аналогія

Уявіть м'яч, що котиться по столу. Якщо він починає з місця і поступово прискорюється — це ease-in. Якщо він вже швидко котиться і поступово гальмує — ease-out. Якщо прискорюється, а потім гальмує — ease-in-out. Весь час рівно — linear. Природній рух у реальному світі завжди "рівний" — тому linear у UI часто виглядає механічно і штучно.

Стандартні ключові слова

.element {
    /* Повільно → швидко → повільно (за замовчуванням) */
    transition-timing-function: ease;

    /* Рівна швидкість — виглядає механічно */
    transition-timing-function: linear;

    /* Повільно → все швидше (елемент "йде") */
    transition-timing-function: ease-in;

    /* Швидко → все повільніше (елемент "прилітає") */
    transition-timing-function: ease-out;

    /* Повільно → швидко → повільно (більш виражено ніж ease) */
    transition-timing-function: ease-in-out;
}
Preview
×
🔒localhost:3000

cubic-bezier() — повний контроль

За кожним ключовим словом (ease, ease-out тощо) стоїть конкретна крива Безьє. cubic-bezier() дозволяє задати власну криву через чотири числа — координати двох контрольних точок:

/* cubic-bezier(x1, y1, x2, y2) */
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); /* те саме, що ease */
transition-timing-function: cubic-bezier(0, 0, 1, 1); /* linear */

/* "Пружинний" ефект — y1 або y2 виходять за [0,1] */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);

/* Різкий вхід, плавне завершення */
transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);

Значення y1 та y2 можуть виходити за межі 0, 1 — це дозволяє створювати "пружинні" ефекти, де анімація спочатку "перестрибує" за цільове значення, а потім повертається. Використовуйте cubic-bezier.com для інтерактивного підбору кривих.

steps() — дискретна анімація

/* steps(кількість-кроків, напрямок) */
transition-timing-function: steps(5, end);

steps() розбиває анімацію на дискретні стрибки замість плавного переходу. Корисно для анімації спрайтів (sprite sheets) або для ефекту "машинопису":

.typewriter {
    overflow: hidden;
    white-space: nowrap;
    width: 0;
    animation: typing 3s steps(30, end) forwards;
}

@keyframes typing {
    to {
        width: 100%;
    }
}

transform — трансформації без Layout

transform (трансформація) — це властивість, яка геометрично змінює елемент (положення, розмір, поворот, нахил), але не впливає на нормальний потік документа і не викликає Layout-тригеру. Саме тому transform — ваш кращий друг при анімаціях.

Трансформації не переміщують елемент в DOM — інші елементи не зрушуються. Вони відбуваються як "накладка" поверх нормального потоку, суто на GPU.

Основні функції transform

translate(x, y) — зміщення відносно поточної позиції:

transform: translate(20px, -10px); /* вправо 20px, вгору 10px */
transform: translateX(50%); /* вправо на 50% ширини самого елемента */
transform: translateY(-100%); /* вгору на 100% висоти елемента (!) */

Зверніть на translateY(-100%): це зміщення відносно самого елемента, а не батька. Це дуже зручно для slid-up/slid-down анімацій, де ви не знаєте точну висоту в пікселях.

scale(x, y) — масштабування відносно transform-origin (за замовчуванням — центр елемента):

transform: scale(1.1); /* збільшити рівномірно на 10% */
transform: scale(1.2, 0.8); /* ширше і нижче (ефект розплющення) */
transform: scaleX(0); /* зменшити тільки по горизонталі до 0 */

rotate(кут) — поворот:

transform: rotate(45deg); /* на 45 градусів за годинниковою */
transform: rotate(-90deg); /* проти годинникової */
transform: rotate(0.25turn); /* то ж саме, але через "оберти" */

skew(x, y) — нахил (skew):

transform: skew(10deg, 5deg); /* Нахил по X та Y */
transform: skewX(15deg);

Комбінування трансформацій

Кілька функцій записуються через пробіл. Порядок має значення — трансформації застосовуються справа наліво:

/* Спочатку move, потім rotate — результат різний! */
transform: translateX(100px) rotate(45deg);
transform: rotate(45deg) translateX(100px);

transform-origin — точка трансформації

За замовчуванням трансформації відбуваються відносно центру елемента. transform-origin змінює цю точку:

/* Поворот навколо лівого верхнього кута */
.door {
    transform-origin: left center;
    transition: transform 0.5s ease;
}
.door:hover {
    transform: rotateY(70deg);
}

/* Масштабування з нижнього центру (ефект "виростання") */
.popup {
    transform-origin: bottom center;
    transform: scale(0);
    transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.popup.visible {
    transform: scale(1);
}
Preview
×
🔒localhost:3000

@keyframes — складні багатокрокові анімації

transition чудово підходить для переходів між двома станами (normal → hover). Але що, якщо потрібно описати складніший сценарій — наприклад, елемент з'являється, затримується, підіймається та зникає? Або нескінченна пульсація? Для цього існують @keyframes.

@keyframes (ключові кадри) — це опис того, що відбувається в конкретний момент анімації. Ви задаєте "знімки" стану елемента в різних точках часу, а браузер самостійно інтерполює проміжні кадри:

/* Оголошення ключових кадрів */
@keyframes slide-in {
    from {
        /* Початковий стан (0%) */
        transform: translateX(-100%);
        opacity: 0;
    }
    to {
        /* Кінцевий стан (100%) */
        transform: translateX(0);
        opacity: 1;
    }
}

/* Застосування анімації до елемента */
.element {
    animation: slide-in 0.5s ease-out forwards;
}

from — скорочення для 0%, to — для 100%. Можна також використовувати відсоткові точки для проміжних станів:

@keyframes bounce {
    0% {
        transform: translateY(0);
    }
    25% {
        transform: translateY(-20px);
    }
    50% {
        transform: translateY(0);
    }
    75% {
        transform: translateY(-10px);
    }
    100% {
        transform: translateY(0);
    }
}

Властивість animation

Застосування @keyframes до елемента відбувається через властивість animation. Вона є скороченням для восьми підвластивостей:

.element {
    /* animation: name duration timing-fn delay iteration direction fill-mode play-state */
    animation: slide-in 0.5s ease-out 0s 1 normal forwards running;
}

Розберемо кожну складову:

animation-name — назва @keyframes правила. Має співпадати з тим, що ви оголосили.

animation-duration — тривалість одного циклу анімації. На відміну від transition, 0s не заблокує анімацію — вона просто стрибне одразу в кінцевий стан.

animation-timing-function — функція часу (та сама ease, cubic-bezier() тощо).

animation-delay — затримка перед початком. Приймає від'ємні значення: animation-delay: -0.5s означає "анімація вже пройшла 0.5 секунди від початку".

animation-iteration-count — кількість повторень. 1 (один раз), 3 (тричі), infinite (нескінченно).

animation-direction — напрямок анімації:

  • normal — завжди від from до to
  • reverse — завжди від to до from
  • alternate — перший цикл from→to, другий to→from, потім знову і т.д. (ефект пінг-понгу)
  • alternate-reverse — те саме, але починає з to→from

animation-fill-mode — стан елемента до та після анімації:

  • none — повертається до початкового стану після закінчення
  • forwards — залишається у стані останнього ключового кадру (to)
  • backwards — одразу приймає стан першого ключового кадру (from), навіть під час delay
  • both — комбінація forwards та backwards

animation-play-staterunning (грає) або paused (пауза). Корисно для зупинки анімації через JavaScript або :hover:

.spinner {
    animation: rotate 1s linear infinite;
}

/* Зупинити анімацію при наведенні */
.spinner:hover {
    animation-play-state: paused;
}
Preview
×
🔒localhost:3000

Практика: Loading Spinner

Спінер завантаження — один із найуживаніших анімаційних елементів. Розберемо три підходи й поясним чому кожен з них влаштований саме так.

Класичний CSS spinner

Ідея: кільце з прозорим сектором, що обертається. Реалізується через border з одним кольоровим сектором і animation: rotate infinite linear.

.spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #e2e8f0; /* Базове кільце */
    border-top-color: #6366f1; /* Кольоровий сектор */
    border-radius: 50%; /* Коло */
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

Чому linear? Бо ми хочемо рівномірне обертання — тут будь-яке прискорення або уповільнення виглядало б дивно.

Пульсуючий спінер (dots)

@keyframes dot-pulse {
    0%,
    80%,
    100% {
        transform: scale(0.4);
        opacity: 0.3;
    }
    40% {
        transform: scale(1);
        opacity: 1;
    }
}

.dot-spinner span {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #6366f1;
    animation: dot-pulse 1.4s ease-in-out infinite;
}

.dot-spinner span:nth-child(2) {
    animation-delay: 0.2s;
}
.dot-spinner span:nth-child(3) {
    animation-delay: 0.4s;
}

Тут animation-delay на кожному span створює хвильовий ефект — три точки пульсують по черзі, з зміщенням 0.2с.

Preview
×
🔒localhost:3000

will-change — підказка браузеру для оптимізації

will-change — це спосіб сказати браузеру: "цей елемент буде анімуватися певними властивостями". Браузер може завчасно перенести елемент на окремий GPU-шар, уникнувши "прогрівання" на першому кадрі анімації:

.animated-card {
    will-change: transform, opacity;
}

Звучить як "завжди використовувати" — але це антипатерн. Кожен will-change-елемент займає додаткову GPU-пам'ять. Якщо ви поставите will-change: transform на 200 карток — браузер резервує GPU-шари для всіх 200 одночасно, що може уповільнити рендеринг.

will-change — ліки, що призначаються по потребі, не профілактично. Додавайте його тільки до елементів, що дійсно анімуються і тільки коли відчуваєте конкретні проблеми з продуктивністю. Видаляйте will-change після завершення анімації через JavaScript. Не ставте will-change: all — це ренегат.

Правильний паттерн — встановлювати will-change динамічно:

/* Правильно: will-change тільки при hover */
.card:hover {
    will-change: transform;
}
// Або через JavaScript — встановити перед анімацією, прибрати після
element.addEventListener('mouseenter', () => {
    element.style.willChange = 'transform'
})
element.addEventListener('animationend', () => {
    element.style.willChange = 'auto' /* Скасувати */
})

prefers-reduced-motion — доступність анімацій

Частина користувачів страждає від вестибулярних розладів (vestibular disorders) — стан, при якому рух у полі зору викликає запаморочення, нудоту та дезорієнтацію. Це — не особливий випадок: за різними оцінками від 0.5% до 35% людей мають чутливість до руху різного ступеня.

Сучасні операційні системи дозволяють увімкнути режим "Зменшити рух" (Reduce Motion): macOS → Accessibility → Display → Reduce Motion; iOS → Settings → Accessibility → Motion → Reduce Motion.

CSS надає media query prefers-reduced-motion, яка дозволяє реагувати на це налаштування:

/* Базові анімації — для всіх */
.btn {
    transition: transform 0.3s ease;
}

.btn:hover {
    transform: translateY(-2px);
}

/* Для користувачів з увімкненим Reduce Motion */
@media (prefers-reduced-motion: reduce) {
    .btn {
        transition: none; /* Вимкнути transition */
    }

    .btn:hover {
        transform: none; /* Або замінити рухову анімацію на статичну зміну */
    }

    /* Зупинити всі нескінченні анімації */
    .spinner {
        animation: none;
    }
}

Правильна стратегія

Не "вимикати всі анімації", а замінювати їх на безпечні альтернативи. Раптова зміна кольору безпечна; рух по ekranu — ні.

/* Mobile-first для анімацій: за замовчуванням без руху */
@media (prefers-reduced-motion: no-preference) {
    /* Тільки для тих, хто НЕ обмежив рух — додай анімації */
    .hero {
        animation: fade-slide-up 0.8s ease-out both;
    }

    .card {
        transition:
            transform 0.25s ease,
            box-shadow 0.25s ease;
    }
}

Цей підхід — "opt-in" для анімацій — є найбільш інклюзивним: спочатку жодних рухів, анімації додаються для тих, хто їх сприймає нормально.


Практика: Hover Cards та Fade-in при скролі

Hover card з підйомом

Класичний ефект картки, що підіймається при наведенні — один із найефективніших за "враження від виконання" на кількість рядків коду:

Preview
×
🔒localhost:3000

Skeleton Loading — анімація очікування

Skeleton screen (екран-скелет) — це placeholder-анімація, що показує "форму" майбутнього контенту поки він завантажується. Психологічно це значно краще за звичайний спінер: користувач бачить, що саме завантажується і де воно з'явиться.

Реалізується через @keyframes із градієнтом, що "рухається":

Preview
×
🔒localhost:3000

Ключовий прийом: background-size: 200% 100% і рухомий background-position. Градієнт ширший за елемент вдвічі, і анімація зсуває його від -200% до 200% — утворюється ефект "блиску", що пробігає зліва направо.


Резюме

Loading diagram...
mindmap
    root((CSS Анімації))
        transition
            Переходи між станами
            4 параметри: property duration timing delay
            Кілька через кому
        Timing Functions
            ease / linear
            ease-in / ease-out / ease-in-out
            cubic-bezier
            steps
        transform
            translate — зміщення
            scale — масштаб
            rotate — поворот
            skew — нахил
            transform-origin
        keyframes
            from → to або %
            animation: name duration...
            fill-mode forwards
            iteration infinite
        GPU та продуктивність
            transform та opacity
            Уникати width height top left
            will-change обережно
        Доступність
            prefers-reduced-motion
            Opt-in стратегія
        Практика
            Hover cards
            Spinners
            Skeleton loading

Завдання для самоперевірки


Попередня стаття: Адаптивний дизайн. Частина 2

Наступна стаття: CSS Custom Properties. Методології. Сучасний CSS

Copyright © 2026