CSS нарешті дозволяє анімувати першу появу елемента: знайомимося з @starting-style
У CSS давно є transition, але в нього була одна неприємна особливість: перехід не запускався під час першої появи елемента на сторінці.
Наприклад, якщо ми додавали повідомлення, модальне вікно, картку товару або інший елемент у DOM через JavaScript, браузер одразу показував його у фінальному стані.
Щоб отримати плавну появу, розробникам доводилося використовувати додаткові класи, requestAnimationFrame(), setTimeout() або примусове читання layout.
Тепер це можна зробити значно простіше.
Для цього в CSS з’явилося правило:
@starting-style
Воно дозволяє описати початковий стан елемента перед його першою появою, після чого звичайний transition виконає плавний перехід до фінального стану.
І найважливіше — сьогодні це вже не експериментальна можливість. @starting-style входить до Baseline 2024, а глобальна підтримка браузерами становить приблизно 90,65%.
Яку проблему вирішує @starting-style
Розглянемо простий приклад.
У нас є елемент:
<div class="notice">
Налаштування успішно збережено
</div>
І ми хочемо, щоб під час появи він плавно піднімався знизу:
.notice {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
Логічно було б припустити, що браузер сам виконає перехід від якогось початкового стану.
Але проблема в тому, що коли елемент тільки з’явився, попереднього стану в нього немає.
Браузер бачить одразу:
opacity: 1;
transform: translateY(0);
Тому переходити фактично немає звідки.
Саме тут і потрібен @starting-style.
Найпростіший приклад @starting-style
Додамо початковий стан:
.notice {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
@starting-style {
.notice {
opacity: 0;
transform: translateY(20px);
}
}
Тепер під час першої появи .notice браузер фактично бачить три речі:
Початковий стан
opacity: 0
translateY(20px)
↓ transition
Фінальний стан
opacity: 1
translateY(0)
І все це працює без JavaScript-коду, який відповідає за запуск самої анімації.
JavaScript усе ще може додати елемент у DOM, але йому більше не потрібно вручну створювати проміжний стан.
Вкладений синтаксис
Є ще один варіант запису.
@starting-style можна вкладати безпосередньо в CSS-селектор:
.notice {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.3s ease,
transform 0.3s ease;
@starting-style {
opacity: 0;
transform: translateY(20px);
}
}
Особисто мені такий варіант подобається більше.
В одному місці ми одразу бачимо:
- фінальний стан;
- transition;
- початковий стан.
Для компонентного CSS це особливо зручно.
Як ми робили це раніше
До появи @starting-style типовий код виглядав приблизно так.
CSS:
.notice {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
.notice.is-visible {
opacity: 1;
transform: translateY(0);
}
JavaScript:
const notice = document.createElement('div');
notice.className = 'notice';
notice.textContent = 'Налаштування успішно збережено';
document.body.appendChild(notice);
requestAnimationFrame(() => {
notice.classList.add('is-visible');
});
Навіщо тут requestAnimationFrame()?
Тому що браузеру потрібно було спочатку відрендерити:
opacity: 0;
а вже потім отримати:
opacity: 1;
Інакше він міг застосувати обидва стани в одному циклі рендерингу — і transition знову не запускався.
З @starting-style це спрощується.
JavaScript:
const notice = document.createElement('div');
notice.className = 'notice';
notice.textContent = 'Налаштування успішно збережено';
document.body.appendChild(notice);
CSS:
.notice {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.3s ease,
transform 0.3s ease;
@starting-style {
opacity: 0;
transform: translateY(20px);
}
}
Готово.
JavaScript займається логікою.
CSS — анімацією.
Саме так і повинно бути.
Де це реально корисно
@starting-style особливо добре підходить для елементів, які з’являються динамічно.
Наприклад:
AJAX-повідомлення
.ajax-message {
opacity: 1;
transform: translateY(0);
transition: 0.3s ease;
@starting-style {
opacity: 0;
transform: translateY(-10px);
}
}
Це можуть бути повідомлення:
Товар додано до кошика
Форму успішно відправлено
Налаштування збережено
Сталася помилка
Toast notifications
.toast {
opacity: 1;
transform: translateX(0);
transition:
opacity 0.25s ease,
transform 0.25s ease;
@starting-style {
opacity: 0;
transform: translateX(30px);
}
}
Під час додавання .toast у DOM він автоматично плавно з’явиться справа.
Картки, які додаються через AJAX
Наприклад, WooCommerce підвантажує товари без перезавантаження сторінки.
Можна зробити:
.product-card {
opacity: 1;
transform: translateY(0) scale(1);
transition:
opacity 0.4s ease,
transform 0.4s ease;
@starting-style {
opacity: 0;
transform: translateY(15px) scale(0.98);
}
}
Після додавання нового елемента в DOM він сам отримає плавну появу.
Без:
element.classList.add('animate');
Без таймерів.
Без requestAnimationFrame().
Popover API + @starting-style
Ще цікавіше @starting-style виглядає разом із сучасним HTML Popover API.
HTML:
<button popovertarget="user-menu">
Відкрити меню
</button>
<div id="user-menu" popover>
<a href="#">Профіль</a>
<a href="#">Налаштування</a>
<a href="#">Вийти</a>
</div>
CSS:
[popover]:popover-open {
opacity: 1;
transform: translateY(0) scale(1);
transition:
opacity 0.2s ease,
transform 0.2s ease;
@starting-style {
opacity: 0;
transform: translateY(-8px) scale(0.96);
}
}
Тепер popover може плавно з’являтися без JavaScript-анімацій.
MDN окремо відзначає popover та <dialog> як одні з основних сценаріїв використання @starting-style.
А що з display: none?
Ось тут стає ще цікавіше.
Історично display був однією з найбільш незручних властивостей для transition.
Наприклад:
.modal {
display: none;
opacity: 0;
}
.modal.active {
display: block;
opacity: 1;
}
Ми могли очікувати плавний opacity, але через display: none елемент фактично не існував у layout.
Сучасний CSS поступово вирішує і цю проблему.
Можна використовувати:
.modal {
transition:
opacity 0.3s,
display 0.3s;
transition-behavior: allow-discrete;
}
У поєднанні з @starting-style це відкриває можливість створювати значно складніші entry/exit transitions без великої кількості JavaScript.
Наприклад:
.modal {
display: none;
opacity: 0;
transform: scale(0.95);
transition:
opacity 0.3s,
transform 0.3s,
display 0.3s allow-discrete;
}
.modal.is-open {
display: block;
opacity: 1;
transform: scale(1);
@starting-style {
opacity: 0;
transform: scale(0.95);
}
}
Тут @starting-style описує стан, з якого починається поява.
Важливо розуміти, що @starting-style стосується саме CSS transitions. Для звичайних @keyframes animations він не потрібен.
@starting-style не замінює JavaScript
Тут є важливий нюанс.
Може здатися, що тепер JavaScript для UI-анімацій взагалі не потрібен.
Це не так.
@starting-style не вирішує бізнес-логіку.
Наприклад, JavaScript усе одно може бути потрібен, щоб:
document.body.appendChild(element);
або:
modal.classList.add('is-open');
або щоб отримати дані через AJAX.
Але раніше JS часто був потрібен ще й для технічного запуску анімації:
element.classList.add('initial');
requestAnimationFrame(() => {
element.classList.add('visible');
});
Ось саме цього коду тепер у багатьох випадках можна позбутися.
Хороший приклад для WordPress
Уявімо WordPress-сайт із кастомною AJAX-формою.
Після успішної відправки ми створюємо повідомлення:
const message = document.createElement('div');
message.className = 'form-success';
message.textContent = 'Дякуємо! Повідомлення відправлено.';
form.appendChild(message);
CSS:
.form-success {
padding: 16px 20px;
border-radius: 10px;
opacity: 1;
transform: translateY(0);
transition:
opacity 0.35s ease,
transform 0.35s ease;
@starting-style {
opacity: 0;
transform: translateY(15px);
}
}
JS взагалі не знає, що елемент анімований.
І це хороший поділ відповідальності:
JavaScript
↓
створює елемент
CSS
↓
вирішує, як він виглядає і з'являється
Ще один приклад — mini cart WooCommerce
У WooCommerce часто потрібно показати повідомлення після додавання товару в кошик.
Наприклад:
.woocommerce-message {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.3s ease,
transform 0.3s ease;
@starting-style {
opacity: 0;
transform: translateY(-15px);
}
}
Якщо повідомлення додається в DOM динамічно, браузер отримує стартовий стан автоматично.
Для кастомних WooCommerce-інтерфейсів це може прибрати помітну кількість допоміжного JS.
Які властивості краще анімувати
Як і з будь-якими CSS transitions, найкраще використовувати властивості, які браузер може обробляти ефективно.
Передусім:
opacity
transform
Наприклад:
@starting-style {
.card {
opacity: 0;
transform: translateY(20px);
}
}
А не намагатися без необхідності анімувати:
width
height
top
left
margin
Особливо якщо таких елементів на сторінці багато.
Для більшості появ UI-компонентів комбінації:
opacity + translate
або:
opacity + scale
цілком достатньо.
Не забуваємо про prefers-reduced-motion
Нові можливості CSS не скасовують accessibility.
Якщо користувач увімкнув у системі зменшення анімацій, це бажано враховувати.
Наприклад:
.card {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.3s ease,
transform 0.3s ease;
@starting-style {
opacity: 0;
transform: translateY(20px);
}
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}
Таким чином ефект залишається для більшості користувачів, але не створює зайвого руху для тих, хто свідомо його вимкнув.
Що буде у старих браузерах?
У цьому і є одна з переваг такого підходу.
Браузер, який не розуміє:
@starting-style
просто не застосує початковий стиль.
Елемент усе одно буде показаний у своєму нормальному фінальному стані.
Тобто замість:
плавної появи
користувач просто побачить:
звичайну появу
Функціональність сайту при цьому не повинна ламатися.
Це хороший приклад progressive enhancement.
Підтримка браузерами
Станом на момент написання цієї статті підтримка вже достатньо хороша для використання в сучасних проєктах.
Can I Use показує приблизно 90,65% глобального покриття.
Підтримуються сучасні версії:
Chrome
Edge
Firefox
Safari
Safari iOS
Samsung Internet
MDN позначає @starting-style як Baseline 2024 — Newly available і вказує, що функція стала доступною в актуальних основних браузерах із серпня 2024 року.
Тому для більшості нових сайтів я вже не бачу причин боятися використовувати її як progressive enhancement.
Важливий нюанс: @starting-style — це не @keyframes
Не варто плутати:
@starting-style
з:
@keyframes
Наприклад, так:
.element {
animation: fade-in 0.3s ease;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
і це:
.element {
opacity: 1;
transition: opacity 0.3s ease;
@starting-style {
opacity: 0;
}
}
можуть виглядати однаково.
Але концептуально це різні речі.
animation запускає окрему CSS-анімацію.
@starting-style дає початковий стан звичайному transition, коли попереднього стилю в елемента ще не було.
І саме тому ця можливість така корисна для компонентів інтерфейсу.
Підсумок
@starting-style — невелике доповнення до CSS, але воно вирішує проблему, з якою frontend-розробники стикалися роками.
Тепер замість конструкцій на кшталт:
element.classList.add('start');
requestAnimationFrame(() => {
element.classList.add('visible');
});
у багатьох випадках достатньо:
.element {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.3s ease,
transform 0.3s ease;
@starting-style {
opacity: 0;
transform: translateY(20px);
}
}
І це хороший приклад того, як сучасний CSS поступово забирає на себе завдання, для яких ще кілька років тому доводилося писати JavaScript.
Особливо корисним @starting-style буде для:
- модальних вікон;
- popover;
- toast-повідомлень;
- AJAX-контенту;
- форм;
- WooCommerce-повідомлень;
- mini cart;
- динамічних Gutenberg-компонентів;
- елементів, які додаються до DOM після завантаження сторінки.
Тож @starting-style уже цілком можна додавати до набору інструментів сучасного frontend-розробника.
Якщо підтримка старих браузерів для проєкту не є критичною — використовуйте.
А якщо є — використовуйте все одно як progressive enhancement: у старому браузері користувач просто не побачить анімацію, але сам інтерфейс продовжить працювати.
en