CSS @starting-style: анімація появи елементів без JavaScript

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: у старому браузері користувач просто не побачить анімацію, але сам інтерфейс продовжить працювати.

Опубліковано в css

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

WP-Hunter

Безпечний вхід до акаунта

Увійдіть, створіть акаунт або відновіть пароль.

З поверненням

Увійдіть, щоб продовжити.