CSS Анімації та Переходи
CSS Анімації та Переходи
Різниця між хорошим і чудовим UX — 200 мілісекунд
Відкрийте сайт Apple, Stripe чи Linear і поводьте мишею по кнопках, картках, навігації. Нічого не відбувається "різко" — все переходить плавно: кнопки "наливаються" кольором при наведенні, картки злегка підіймаються, елементи з'являються з легким ковзанням. Ці переходи настільки природні, що ви майже їх не помічаєте. Але якщо їх прибрати — інтерфейс одразу стає "пластиковим", механічним.
Це і є мистецтво CSS-анімацій: добрі анімації не кричать "подивіться на мене!", вони тихо роблять взаємодію приємнішою. Погані анімації — навпаки, заважають, відволікають, дратують.
У цій статті ми розберемо дві системи анімування в CSS: transition для простих станів і @keyframes + animation для складних сценаріїв. Але перш ніж писати код — зрозуміємо, чому анімації взагалі потрібні і як браузер їх виконує під капотом.
Навіщо анімації в інтерфейсах?
Перш ніж писати будь-який анімаційний код, варто зрозуміти психологічну та функціональну роль анімацій у UI. Це не декоративна надмірність — це комунікація.
Анімації як мову системи
Людське зорове сприйняття еволюційно заточено на рух. Рух означає щось важливе: небезпека, їжа, інша істота. У цифровому інтерфейсі цей механізм можна використати на користь користувача:
- Показати взаємозв'язок. Елемент, що "збирається" в іконку кошика, показує, що товар доданий і пов'язаний із кошиком. Без анімації — це просто зміна числа.
- Дати зворотний зв'язок. Кнопка, що "пружинить" при кліку, підтверджує: "так, я вас почула". Без неї — система здається задумливою або зламаною.
- Орієнтувати в просторі. Бічна панель, що виїжджає зліва, підказує: коли вона закриється — вона "поїде" назад ліворуч. Якщо б вона просто зникала — користувач губився б.
- Привернути увагу до важливого. Помилка, що м'яко "підстрибує", привертає погляд без агресивного блимання.
- Заповнити час очікування. Spinner або skeleton-екран під час завантаження знижує суб'єктивне відчуття часу очікування.
Правила хорошої анімації
Перш ніж робити анімацію — поставте собі три питання:
- Чи вона комунікує щось важливе? Якщо відповідь "ні, просто красиво" — ймовірно, анімацію краще прибрати або спростити.
- Чи вона не відволікає від задачі? Анімація фону, що постійно рухається, може бути чудовою на лендингу і катастрофою у робочому дашборді.
- Чи вона доступна? Частина користувачів має вестибулярні розлади, і надмірний рух викликає у них фізичний дискомфорт. Ми обов'язково розглянемо
prefers-reduced-motion.
Як браузер рендерить сторінку: конвеєр рендеру
Щоб розуміти, які CSS-властивості безпечно анімувати, а які — ні, потрібно знати, як браузер малює сторінку. Цей процес називається rendering pipeline (конвеєр рендеру) і складається з кількох кроків:
Кожен крок конвеєру дорогий, але вони не однаково дорогі:
- 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.
| Властивість | Крок конвеєру | Безпечна для анімації? |
|---|---|---|
transform | Composite | ✅ GPU-прискорена |
opacity | Composite | ✅ GPU-прискорена |
filter | Paint + Composite | ⚠️ Може бути важкою |
color, background-color | Paint | ⚠️ Прийнятно |
width, height | Layout | ❌ Не рекомендовано |
margin, padding | Layout | ❌ Не рекомендовано |
top, left | Layout | ❌ Замінюйте на 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;
}
Кожна властивість може мати власну тривалість і функцію часу — це дозволяє тонко контролювати відчуття від взаємодії.
<div class="transitions-demo">
<button class="btn-demo btn-simple">Простий (0.2s ease)</button>
<button class="btn-demo btn-scale">Scale + shadow</button>
<button class="btn-demo btn-multi">Кілька властивостей</button>
</div>
.transitions-demo {
display: flex;
gap: 1rem;
flex-wrap: wrap;
padding: 1.5rem;
background: #f8fafc;
border-radius: 12px;
font-family: system-ui, sans-serif;
justify-content: center;
}
.btn-demo {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
font-family: inherit;
color: white;
}
.btn-simple {
background: #6366f1;
transition: background-color 0.2s ease;
}
.btn-simple:hover {
background: #4338ca;
}
.btn-scale {
background: #10b981;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.btn-scale:hover {
transform: translateY(-3px) scale(1.04);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4);
}
.btn-multi {
background: #f59e0b;
transition:
background-color 0.3s ease,
transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.3s ease;
}
.btn-multi:hover {
background: #d97706;
transform: translateY(-4px);
box-shadow: 0 10px 30px rgba(245, 158, 11, 0.4);
}
Наведіть курсор на кожну кнопку і відчуйте різницю у характері переходу — навіть у межах простого 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;
}
<div class="timing-demo">
<div class="timing-row">
<span class="timing-label">ease</span>
<div class="timing-track">
<div class="timing-ball ease-ball">●</div>
</div>
</div>
<div class="timing-row">
<span class="timing-label">linear</span>
<div class="timing-track">
<div class="timing-ball linear-ball">●</div>
</div>
</div>
<div class="timing-row">
<span class="timing-label">ease-in</span>
<div class="timing-track">
<div class="timing-ball ease-in-ball">●</div>
</div>
</div>
<div class="timing-row">
<span class="timing-label">ease-out</span>
<div class="timing-track">
<div class="timing-ball ease-out-ball">●</div>
</div>
</div>
<div class="timing-row">
<span class="timing-label">ease-in-out</span>
<div class="timing-track">
<div class="timing-ball ease-in-out-ball">●</div>
</div>
</div>
<div class="timing-row">
<span class="timing-label">cubic-bezier<br />(spring)</span>
<div class="timing-track">
<div class="timing-ball spring-ball">●</div>
</div>
</div>
</div>
<p style="text-align:center;font-family:system-ui,sans-serif;font-size:0.8rem;color:#64748b;margin-top:0.5rem">
Наведіть курсор на блок, щоб запустити анімацію
</p>
.timing-demo {
background: #f8fafc;
border-radius: 12px;
padding: 1.25rem;
font-family: system-ui, sans-serif;
}
.timing-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.timing-label {
font-size: 0.75rem;
font-weight: 600;
color: #475569;
width: 90px;
flex-shrink: 0;
text-align: right;
line-height: 1.3;
}
.timing-track {
flex: 1;
height: 32px;
background: #e2e8f0;
border-radius: 16px;
position: relative;
overflow: hidden;
cursor: pointer;
}
.timing-ball {
position: absolute;
left: 4px;
top: 50%;
transform: translateY(-50%);
font-size: 1.1rem;
transition-property: left;
transition-duration: 1.2s;
line-height: 1;
color: #6366f1;
}
.ease-ball {
transition-timing-function: ease;
}
.linear-ball {
transition-timing-function: linear;
}
.ease-in-ball {
transition-timing-function: ease-in;
}
.ease-out-ball {
transition-timing-function: ease-out;
}
.ease-in-out-ball {
transition-timing-function: ease-in-out;
}
.spring-ball {
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
color: #f59e0b;
}
.timing-demo:hover .timing-ball {
left: calc(100% - 28px);
}
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);
}
<div class="transform-showcase">
<div class="t-group">
<p class="t-label">translate(-8px, -8px)</p>
<div class="t-box translate-box">Наведіть</div>
</div>
<div class="t-group">
<p class="t-label">scale(1.15)</p>
<div class="t-box scale-box">Наведіть</div>
</div>
<div class="t-group">
<p class="t-label">rotate(15deg)</p>
<div class="t-box rotate-box">Наведіть</div>
</div>
<div class="t-group">
<p class="t-label">scale(1.1) + shadow</p>
<div class="t-box combo-box">Наведіть</div>
</div>
</div>
.transform-showcase {
display: flex;
gap: 1rem;
flex-wrap: wrap;
padding: 1.5rem;
background: #f1f5f9;
border-radius: 12px;
font-family: system-ui, sans-serif;
justify-content: center;
}
.t-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
}
.t-label {
margin: 0;
font-size: 0.72rem;
color: #64748b;
font-weight: 600;
text-align: center;
}
.t-box {
width: 100px;
height: 100px;
background: #6366f1;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 0.8rem;
cursor: pointer;
transition:
transform 0.25s ease,
box-shadow 0.25s ease;
}
.translate-box:hover {
transform: translate(-8px, -8px);
}
.scale-box:hover {
transform: scale(1.15);
}
.rotate-box:hover {
transform: rotate(15deg);
}
.combo-box:hover {
transform: scale(1.1);
box-shadow: 0 12px 35px rgba(99, 102, 241, 0.45);
}
@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доtoreverse— завжди відtoдоfromalternate— перший циклfrom→to, другийto→from, потім знову і т.д. (ефект пінг-понгу)alternate-reverse— те саме, але починає зto→from
animation-fill-mode — стан елемента до та після анімації:
none— повертається до початкового стану після закінченняforwards— залишається у стані останнього ключового кадру (to)backwards— одразу приймає стан першого ключового кадру (from), навіть під часdelayboth— комбінаціяforwardsтаbackwards
animation-play-state — running (грає) або paused (пауза). Корисно для зупинки анімації через JavaScript або :hover:
.spinner {
animation: rotate 1s linear infinite;
}
/* Зупинити анімацію при наведенні */
.spinner:hover {
animation-play-state: paused;
}
<div class="keyframes-demo">
<div class="kf-row">
<span class="kf-label">fade-in (forwards)</span>
<div class="kf-scene">
<div class="kf-element fade-in-el">Текст</div>
</div>
</div>
<div class="kf-row">
<span class="kf-label">slide-up</span>
<div class="kf-scene">
<div class="kf-element slide-up-el">Слайд</div>
</div>
</div>
<div class="kf-row">
<span class="kf-label">pulse (infinite)</span>
<div class="kf-scene">
<div class="kf-dot pulse-el"></div>
</div>
</div>
<div class="kf-row">
<span class="kf-label">bounce (alternate)</span>
<div class="kf-scene">
<div class="kf-ball bounce-el">🏀</div>
</div>
</div>
</div>
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes pulse-dot {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.4);
opacity: 0.6;
}
}
@keyframes bounce-ball {
from {
transform: translateY(0);
}
to {
transform: translateY(-30px);
}
}
.keyframes-demo {
background: #f8fafc;
border-radius: 12px;
padding: 1.25rem;
font-family: system-ui, sans-serif;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.kf-row {
display: flex;
align-items: center;
gap: 1rem;
}
.kf-label {
font-size: 0.8rem;
font-weight: 600;
color: #475569;
width: 140px;
flex-shrink: 0;
}
.kf-scene {
flex: 1;
background: #e2e8f0;
border-radius: 8px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.kf-element {
background: #6366f1;
color: white;
padding: 0.4rem 1rem;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
}
.fade-in-el {
animation: fade-in 1.5s ease infinite alternate;
}
.slide-up-el {
animation: slide-up 1.2s ease-out infinite;
animation-delay: 0.5s;
}
.kf-dot {
width: 18px;
height: 18px;
border-radius: 50%;
background: #6366f1;
animation: pulse-dot 1.2s ease-in-out infinite;
}
.kf-ball {
font-size: 1.4rem;
animation: bounce-ball 0.6s ease-in alternate infinite;
}
Практика: 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с.
<div class="spinners-showcase">
<div class="spinner-item">
<div class="classic-spinner"></div>
<span>Classic</span>
</div>
<div class="spinner-item">
<div class="dot-spinner">
<span></span>
<span></span>
<span></span>
</div>
<span>Dots</span>
</div>
<div class="spinner-item">
<div class="bar-spinner">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<span>Bars</span>
</div>
<div class="spinner-item">
<div class="ring-spinner"></div>
<span>Ring</span>
</div>
</div>
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes dot-pulse {
0%,
80%,
100% {
transform: scale(0.4);
opacity: 0.3;
}
40% {
transform: scale(1);
opacity: 1;
}
}
@keyframes bar-grow {
0%,
40%,
100% {
transform: scaleY(0.4);
}
20% {
transform: scaleY(1);
}
}
@keyframes ring-spin {
0% {
transform: rotate(0deg);
stroke-dashoffset: 220;
}
50% {
stroke-dashoffset: 55;
}
100% {
transform: rotate(720deg);
stroke-dashoffset: 220;
}
}
.spinners-showcase {
display: flex;
gap: 2rem;
padding: 2rem;
background: #f8fafc;
border-radius: 12px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
font-family: system-ui, sans-serif;
}
.spinner-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
font-size: 0.8rem;
color: #64748b;
font-weight: 600;
}
.classic-spinner {
width: 36px;
height: 36px;
border: 4px solid #e2e8f0;
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.dot-spinner {
display: flex;
gap: 0.4rem;
align-items: center;
height: 36px;
}
.dot-spinner span {
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;
}
.bar-spinner {
display: flex;
gap: 0.3rem;
align-items: flex-end;
height: 36px;
}
.bar-spinner span {
display: block;
width: 6px;
height: 28px;
background: #6366f1;
border-radius: 3px;
transform-origin: bottom;
animation: bar-grow 1.2s ease-in-out infinite;
}
.bar-spinner span:nth-child(1) {
animation-delay: 0s;
}
.bar-spinner span:nth-child(2) {
animation-delay: 0.1s;
}
.bar-spinner span:nth-child(3) {
animation-delay: 0.2s;
}
.bar-spinner span:nth-child(4) {
animation-delay: 0.3s;
}
.ring-spinner {
width: 36px;
height: 36px;
border: 4px solid #e2e8f0;
border-radius: 50%;
border-top-color: #6366f1;
border-right-color: #818cf8;
animation: spin 1s ease-in-out infinite;
}
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 з підйомом
Класичний ефект картки, що підіймається при наведенні — один із найефективніших за "враження від виконання" на кількість рядків коду:
<div class="cards-grid">
<article class="hover-card">
<div class="hcard-icon">🚀</div>
<h3>Запуск</h3>
<p>Класичний hover-ефект: translateY + box-shadow через transition.</p>
<a href="#" class="hcard-link">Детальніше →</a>
</article>
<article class="hover-card">
<div class="hcard-icon">⚡</div>
<h3>Швидкість</h3>
<p>Тільки transform та box-shadow — GPU-прискорено, без Layout.</p>
<a href="#" class="hcard-link">Детальніше →</a>
</article>
<article class="hover-card">
<div class="hcard-icon">💎</div>
<h3>Якість</h3>
<p>Cubic-bezier з пружинним ефектом надає "живого" відчуття.</p>
<a href="#" class="hcard-link">Детальніше →</a>
</article>
</div>
@keyframes card-appear {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.cards-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
padding: 1.25rem;
background: #f1f5f9;
border-radius: 12px;
font-family: system-ui, sans-serif;
}
.hover-card {
background: white;
border-radius: 12px;
padding: 1.25rem;
border: 1px solid #e2e8f0;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.35rem;
/* Анімація появи при завантаженні */
animation: card-appear 0.5s ease-out both;
/* Плавний перехід для hover */
transition:
transform 0.25s cubic-bezier(0.34, 1.3, 0.64, 1),
box-shadow 0.25s ease;
}
.hover-card:nth-child(2) {
animation-delay: 0.1s;
}
.hover-card:nth-child(3) {
animation-delay: 0.2s;
}
.hover-card:hover {
transform: translateY(-6px);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.12);
}
.hcard-icon {
font-size: 1.75rem;
margin-bottom: 0.25rem;
}
.hover-card h3 {
margin: 0;
font-size: 1rem;
color: #1e293b;
}
.hover-card p {
margin: 0;
font-size: 0.82rem;
color: #64748b;
line-height: 1.5;
flex: 1;
}
.hcard-link {
color: #6366f1;
text-decoration: none;
font-size: 0.82rem;
font-weight: 600;
margin-top: 0.25rem;
transition: gap 0.2s ease;
display: inline-block;
}
.hcard-link:hover {
text-decoration: underline;
}
Skeleton Loading — анімація очікування
Skeleton screen (екран-скелет) — це placeholder-анімація, що показує "форму" майбутнього контенту поки він завантажується. Психологічно це значно краще за звичайний спінер: користувач бачить, що саме завантажується і де воно з'явиться.
Реалізується через @keyframes із градієнтом, що "рухається":
<div class="skeleton-card">
<div class="skel-img"></div>
<div class="skel-content">
<div class="skel-line skel-title"></div>
<div class="skel-line skel-text"></div>
<div class="skel-line skel-text short"></div>
<div class="skel-meta">
<div class="skel-avatar"></div>
<div class="skel-author-info">
<div class="skel-line skel-name"></div>
<div class="skel-line skel-date"></div>
</div>
</div>
</div>
</div>
@keyframes skeleton-shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.skeleton-card {
background: white;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e2e8f0;
font-family: system-ui, sans-serif;
max-width: 380px;
margin: 0 auto;
}
/* Базові стилі для skeleton-блоків */
.skel-img,
.skel-line,
.skel-avatar {
background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 6px;
}
.skel-img {
height: 180px;
border-radius: 0;
}
.skel-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.skel-line {
height: 14px;
}
.skel-title {
height: 20px;
width: 70%;
}
.skel-text {
width: 100%;
}
.skel-text.short {
width: 55%;
}
.skel-meta {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 0.5rem;
}
.skel-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
}
.skel-author-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.skel-name {
width: 60%;
height: 12px;
}
.skel-date {
width: 40%;
height: 10px;
}
Ключовий прийом: background-size: 200% 100% і рухомий background-position. Градієнт ширший за елемент вдвічі, і анімація зсуває його від -200% до 200% — утворюється ефект "блиску", що пробігає зліва направо.
Резюме
Завдання для самоперевірки
Завдання 1.1. Реалізуйте набір кнопок (Primary, Secondary, Danger), кожна з яких має власний hover-ефект:
- Primary: зміна кольору фону + невеликий підйом (
translateY(-2px)) - Secondary: зміна кольору рамки + фону
- Danger: лише зміна кольору фону, але функція часу —
ease-in(щоб надати "важкого" відчуття)
Для всіх — плавний перехід через transition, тільки GPU-безпечні властивості.
Завдання 1.2. Виправте: чому ця анімація спричиняє Layout-тригер і як це виправити зі збереженням ефекту?
.card:hover {
top: -5px; /* Ефект підйому */
}
Завдання 1.3. Поясніть словами: у чому різниця між animation-fill-mode: forwards та animation-fill-mode: backwards? У якому реальному кейсі знадобиться кожен?
Завдання 2.1. Notification badge. Реалізуйте іконку дзвіночка з числом. Коли число збільшується (клас .has-new додається через JS), дзвіночок виконує анімацію "дзвону" — hinge-like коливання через rotate() та animation-direction: alternate. Після 3 повторів — зупиняється.
Завдання 2.2. Skeleton card. Реалізуйте skeleton-екран для списку з 3 постів блогу. Кожна картка: аватар (коло) + заголовок (рядок 70%) + 2 рядки тексту + дата. Анімація мерехтіння (shimmer) без JavaScript.
Завдання 2.3. Staggered list. Реалізуйте список із 6 елементів, де кожен наступний елемент з'являється із затримкою 0.1s після попереднього (slide-up + fade-in). Використовуйте animation-delay та animation-fill-mode: both.
Завдання 3.1 (Міні-проєкт). Toast Notification System.
Реалізуйте повну систему toast-сповіщень:
HTML-структура:
<div class="toast-container">
<!-- position: fixed; bottom-right -->
<div class="toast toast-success">✅ Збережено успішно!</div>
<div class="toast toast-error">❌ Помилка з'єднання</div>
<div class="toast toast-info">ℹ️ Нові оновлення доступні</div>
</div>
Анімаційні вимоги:
- Появлення —
slide-in-right: справа + fade-in за 0.35scubic-bezier(0.34, 1.3, 0.64, 1)(пружинний ефект) - Автовидалення — кожен toast автоматично fade-out + slide-right через 4s
- Progress bar — тонка смуга внизу кожного toast, що "спустошується" за 4s через
animation: progress 4s linear forwards - Hover pause — при наведенні всі анімації ставляться на паузу (
animation-play-state: paused) prefers-reduced-motion— при увімкненому зменшенні руху: тільки fade (без slide), тривалість 0.15s
Попередня стаття: Адаптивний дизайн. Частина 2
Наступна стаття: CSS Custom Properties. Методології. Сучасний CSS
Позиціонування в CSS. Z-index. Stacking Context
Глибокий розгляд position static, relative, absolute, fixed, sticky. Z-index та контекст накладання (stacking context). Практика: tooltip, modal overlay, sticky header, dropdown menu.
Адаптивний дизайн. Media Queries. Частина 1
Основи адаптивного дизайну: viewport meta-тег, синтаксис @media, типи та features, breakpoints, Mobile-first vs Desktop-first підхід, responsive images, srcset та picture.