CSS @starting-style и allow-discrete. Оживляем интерфейс без JavaScript

В CSS разработчики традиционно работали с двумя основными свойствами для создания анимаций:

  1. transition: определяет плавный переход между двумя состояниями элемента при изменении CSS-свойств. Идеально подходит для простых, однонаправленных изменений.
  2. animation в связке с @keyframes: позволяет создавать более сложные, многоэтапные анимации с полным контролем над каждым ключевым кадром.

Оба подхода позволяют настраивать длительность анимации (duration), задержку перед ее началом (delay) и функцию плавности (easing), которая определяет, как вычисляются промежуточные состояния между ключевыми кадрами. Этот процесс расчета промежуточных кадров называется «твининг».

Пример 1

Простой переход с настройкой длительности и функции плавности.

.element {
    transition: opacity 0.5s ease-in-out;
}

.element:hover {
    opacity: 0.7;
}

Однако долгое время существенным ограничением CSS-анимаций была работа с так называемыми дискретными свойствами.

CSS @starting-style и allow-discrete. Оживляем интерфейс без JavaScript

Проблема дискретных значений: почему нельзя было анимировать display?

Ключевой проблемой в CSS-анимациях долгое время оставалась работа с дискретными значениями.

Чтобы понять эту сложность, нужно разобраться в концепции ключевых кадров. Ключевой кадр (keyframe) — это фиксация состояния элемента в конкретный момент времени. Чтобы создать анимацию, мы задаем начальное (точка А) и конечное (точка Б) состояния, а браузер вычисляет все промежуточные шаги. Этот процесс прекрасно работает для непрерывных, числовых значений.

Рассмотрим классический пример со свойством opacity:

  • Это непрерывное свойство, принимающее значения от 0 (полная прозрачность) до 1 (полная непрозрачность).
  • Если анимация длится 1000 мс, и мы меняем opacity с 0 на 1, браузер легко рассчитает состояние на 250 мс (opacity: 0.25), на 500 мс (opacity: 0.5) и так далее.

Совершенно иначе обстоит дело со свойством display:

  • Это дискретное свойство. Его значения (например, block, flex, grid, none) не имеют математического продолжения друг в друге.
  • Не существует такого понятия, как display: 0.5 — элемент либо отображается по правилам flex, либо не отображается (none).
  • Исторически CSS не мог анимировать переход между display: none и display: block (или любым другим видимым значением). Элемент мгновенно появлялся или исчезал, что разрывало плавность интерфейса.

Для обхода этой проблемы разработчики использовали комбинации свойств, например, анимировали opacity с 0 до 1, а затем, по завершении анимации через JavaScript, меняли display на none. Это решение было неидеальным, требовало лишнего кода и усложняло логику.

transition-behavior: allow-discrete — ключ к анимации дискретных свойств

Свойство transition-behavior со значением allow-discrete стало настоящим прорывом, предоставив CSS явное разрешение на анимацию дискретных значений.

Это составное свойство, которое используется внутри общего правила transition. Когда вы применяете allow-discrete к свойству, браузер понимает, что ему нужно обрабатывать переход между его дискретными состояниями как анимацию, а не как мгновенное переключение.

Давайте посмотрим, как это работает на практике. Допустим, мы хотим плавно скрыть и показать модальное окно, анимируя не только прозрачность, но и свойство visibility. Свойство visibility, в отличие от display, может принимать значения visible и hidden, но также является дискретным.

Пример 2

Показ/скрытие модального окна.

.modal {
    /* Определяем переход для непрерывных свойств */
    transition: opacity 0.3s ease-out, transform 0.3s ease-out;
    /* Явно разрешаем анимацию дискретного свойства visibility */
    transition-behavior: allow-discrete;
    /* Или можно задать так для конкретных свойств: */
    /* transition: opacity 0.3s ease-out, transform 0.3s ease-out, visibility 0.3s ease-out;
       transition-behavior: allow-discrete; */
    opacity: 1;
    visibility: visible;
}

.modal.is-hidden {
    opacity: 0;
    transform: translateY(20px);
    visibility: hidden;
}

В этом примере при добавлении класса .is-hidden к модальному окну будут плавно анимированы три свойства одновременно: прозрачность (opacity), вертикальное смещение (transform) и видимость (visibility). Без allow-discrete свойство visibility переключилось бы мгновенно в момент начала анимации, что могло бы прервать отображение.

@starting-style: определяем отправную точку для анимации

Правило @starting-style решает другую давнюю проблему — анимацию появления элемента в DOM.

Раньше, если вы хотели, чтобы элемент плавно появлялся при загрузке страницы или при добавлении в разметку, вам приходилось использовать JavaScript для добавления класса с анимацией после рендера, либо применять хитрые приемы с @keyframes. @starting-style позволяет напрямую в CSS определить стили, с которых начнется анимация элемента в момент его первого появления.

Проще говоря, @starting-style — это ваш способ задать собственную, альтернативную «точку А» для первоначального рендеринга элемента, отличную от стандартных стилей браузера.

Пример 3

Задаем начало отрисовки стилей.

/* Стили по умолчанию для элемента */
.fade-in-element {
    opacity: 1;
    transform: translateY(0);
    transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}

/* Стили, с которых элемент начнет отрисовываться.
   Браузер сделает переход из этого состояния в состояние .fade-in-element */
@starting-style {
    .fade-in-element {
        opacity: 0;
        transform: translateY(20px);
    }
}

Теперь каждый элемент с классом .fade-in-element при вставке в страницу будет не просто появляться, а плавно «выезжать» снизу с увеличением прозрачности. Это мощный инструмент для создания целостного анимационного повествования при загрузке страницы.

Комбинируем инструменты: комплексный пример плавного появления контента

Давайте создадим комплексное решение для плавного появления всего контента на странице, используя оба новых инструмента. Наша цель — чтобы все элементы (заголовки, параграфы, карточки) не просто отображались, а грациозно проявлялись при загрузке, создавая современное и приятное впечатление.

Пример 4

Плавное появление контента всей страницы.

<!DOCTYPE html>
<html lang="ru">
<head>
    <style>
        /* 1. Базовый сброс и разрешение на дискретные переходы для всех элементов */
        * {
            /* Анимируем три свойства */
            transition: opacity 0.6s ease-out, transform 0.6s ease-out, visibility 0.6s ease-out;
            /* Разрешаем анимацию дискретного свойства visibility */
            transition-behavior: allow-discrete;
        }

        /* 2. Определяем стартовый стиль для ВСЕХ элементов при их появлении в DOM */
        /* Важно: оборачиваем в медиа-запрос для уважения настроек пользователя */
        @media (prefers-reduced-motion: no-preference) {
            @starting-style {
                * {
                    /* Начальное состояние: прозрачный, смещенный вниз и скрытый */
                    opacity: 0;
                    transform: translateY(20px);
                    visibility: hidden;
                }
            }
        }

        /* 3. Финальное состояние, в которое браузер сделает переход */
        /* Это стили по умолчанию, которые браузер применяет сам */
        body {
            opacity: 1;
            transform: translateY(0);
            visibility: visible;
        }

        /* 4. Дополнительно: можем добавить задержку для дочерних элементов для эффекта каскада */
        .content>*:nth-child(1) { transition-delay: 0.05s; }
        .content>*:nth-child(2) { transition-delay: 0.1s; }
        .content>*:nth-child(3) { transition-delay: 0.15s; }
        /* ... и так далее ... */
    </style>
</head>
<body>
    <div class="content">
        <h1>Заголовок страницы</h1>
        <p>Этот текст плавно появится на экране.</p>
        <div class="card">Карточка с контентом также будет анимирована.</div>
    </div>
</body>
</html>

Что происходит в этом коде:

  1. Мы применяем глобальное правило перехода для всех элементов (*), включая анимацию visibility с разрешением allow-discrete.
  2. В блоке @starting-style, обернутом в медиа-запрос prefers-reduced-motion (что является обязательной практикой доступности), мы задаем для всех элементов начальное состояние: скрытое, прозрачное и смещенное.
  3. Когда страница загружается, браузер берет эти стартовые стили из @starting-style и анимирует переход к стандартным стилям элемента (в данном случае — к стилям body, которые наследуются), которые имеют opacity: 1, transform: none и visibility: visible.
  4. Добавляя небольшие задержки (transition-delay) для дочерних элементов, мы создаем эффект последовательного, каскадного появления, который выглядит профессионально и динамично.

Для автоматической плавной анимации элементов, добавляемых через JS (сообщения, уведомления), используйте комбинацию @starting-style с селектором :has(). Это позволяет задавать стартовые прозрачные стили новым элементам конкретного контейнера, полностью исключив JS из логики анимаций. Все новое содержимое будет появляться плавно средствами CSS.

Робот собирает из блоков сайт

Собери свой код. Запусти сайт!

От наброска на салфетке до первого работающего лендинга. Наш онлайн-курс «Веб-верстка с нуля и до профессионала» — это интенсивный трек, где ты не будешь зубрить теорию, а с первого дня начнешь превращать идеи в чистый HTML и CSS.

Собери свой первый проект под руководством практикующих разработчиков.

Подробнее о курсе

Практическое применение: создание идеального модального окна

Одно из самых востребованных применений этих технологий — создание модальных (всплывающих) окон.

Классическая проблема: модальное окно красиво появляется (анимируется opacity и transform), но при закрытии исчезает мгновенно, потому что для скрытия используется display: none, который нельзя было анимировать. Решаем ее элегантно.

Пример 5

Плавное закрытие модального окна.

<!-- HTML -->
<button id="btn-open-myModal">Открыть окно</button>
<div class="myModal" id="myModal" aria-hidden="true">
    <div class="myModal__content">
        <h3>Здесь пишем заголовок</h3>
        <p>Контент модального окна.</p>
        <button id="btn-close-myModal">x</button>
    </div>
</div>
/* CSS */
.myModal {
    /* Позиционирование на весь экран */
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.5);
    /* Центрирование содержимого */
    display: flex;
    align-items: center;
    justify-content: center;
    /* Изначально окно скрыто. Обратите внимание: display: none */
    display: none;
    /* !!! Ключевой момент: мы НЕ анимируем display напрямую */
}

/* Класс, который будет добавляться/удаляться JS для показа */
.myModal.open {
    display: flex; /* Меняем дискретное свойство display */
}

/* Слой-оверлей модалки, который и будет анимироваться */
.myModal::before {
    content: '';
    position: absolute;
    inset: 0;
    background-color: inherit;
    /* Определяем анимацию для оверлея */
    opacity: 0;
    transition: opacity 0.3s ease-out, visibility 0.3s ease-out;
    transition-behavior: allow-discrete;
    /* Связываем видимость оверлея с родительским display */
    visibility: hidden;
}

/* Когда модалка открыта, анимируем оверлей */
.myModal.open::before {
    opacity: 1;
    visibility: visible;
}

/* Стили для самого контента модалки */
.myModal__content {
    background: white;
    padding: 2rem;
    border-radius: 8px;
    position: relative;
    z-index: 1;
    /* Анимация для контента */
    opacity: 0;
    transform: scale(0.9);
    transition: opacity 0.3s ease-out 0.1s, transform 0.3s ease-out 0.1s;
    /* Небольшая задержка для эффекта последовательности */
}

.myModal.open .myModal__content {
    opacity: 1;
    transform: scale(1);
}

/* !!! Магия для плавного ИСЧЕЗНОВЕНИЯ: @starting-style для обратного перехода */
@starting-style {
    .myModal.open::before {
        opacity: 0;
        visibility: hidden;
    }
    .myModal.open .myModal__content {
        opacity: 0;
        transform: scale(0.9);
    }
}
// JavaScript (минимальный, только для переключения класса)
const btn_open = document.getElementById('btn-open-myModal');
const btn_close = document.getElementById('btn-close-myModal');
const myModal = document.getElementById('myModal');

btn_open.addEventListener('click', () => {
    myModal.classList.add('open');
    myModal.setAttribute('aria-hidden', 'false');
});

btn_close.addEventListener('click', () => {
    myModal.classList.remove('open');
    myModal.setAttribute('aria-hidden', 'true');
});

Разберем логику:

  1. Основной контейнер .myModal управляется свойством display (через класс .open). Мы его не анимируем.
  2. Всю анимацию (прозрачность фона и масштаб контента) мы вешаем на псевдоэлемент ::before (для фона) и на блок .myModal__content.
  3. Секрет плавного закрытия кроется в @starting-style для класса .myModal.open. Когда JavaScript удаляет класс .open, браузер смотрит на стартовые стили для этого состояния (которые мы определили в @starting-style как opacity: 0) и анимирует переход от текущего видимого состояния к этим стартовым стилям, а уже потом применяется display: none. Это создает идеальную последовательность: сначала фон и контент плавно исчезают, и только затем элемент физически убирается из потока.

Будущее CSS-анимаций и рекомендации по использованию

Новые возможности CSS открывают путь к созданию невероятно сложных и красивых интерфейсных анимаций, которые раньше были прерогативой JavaScript-библиотек. Тенденция ясна: CSS становится все более выразительным и самодостаточным языком для описания взаимодействий.

Полезные советы разработчикам

1. Всегда учитывайте prefers-reduced-motion. Это критически важно для доступности. Пользователи с вестибулярными расстройствами могут плохо переносить анимацию.


@media (prefers-reduced-motion: no-preference) {
    /* Ваши сложные анимации здесь */
}

2. Используйте will-change с умом. Это свойство может подсказать браузеру, какие свойства планируется анимировать, чтобы он заранее оптимизировал рендеринг. Но не применяйте его ко всем элементам подряд — это расходует ресурсы.

.element-to-animate {
    will-change: opacity, transform;
}

3. Сочетайте новые методы с классическими. @starting-style и allow-discrete прекрасно работают вместе с @keyframes, animation и существующими transition.

4. Тестируйте производительность. Анимация свойств, влияющих на геометрию страницы (например, width, height, margin), может вызывать дорогостоящие пересчеты макета. Старайтесь анимировать transform и opacity, так как они часто обрабатываются отдельным, более оптимизированным композитором потока браузера.

Заключение

transition-behavior: allow-discrete и @starting-style — это не просто новые свойства, а смена парадигмы в подходе к проектированию переходов в веб-интерфейсах. Они позволяют разработчикам мыслить более целостно, создавая бесшовные анимационные сценарии от момента загрузки страницы до каждого микровзаимодействия, что в конечном итоге приводит к созданию продуктов высочайшего качества с захватывающим пользовательским опытом.

Хотите, чтобы ваш сайт ожил с помощью подобных современных и плавных анимаций? Мы создаем сайты с динамичными, интуитивно понятными интерфейсами, где каждая деталь работает на ваши бизнес-цели. Закажите разработку сайта или отдельного анимированного компонента у нас — и мы превратим вашу идею в цифровую реальность, которая впечатлит ваших клиентов. Свяжитесь с нами для обсуждения проекта!

Теги: