Плавное появление блока в CSS: как анимировать display: none
Блок с display: none можно показать и скрыть плавно средствами CSS. Разбираем @starting-style и allow-discrete на рабочих примерах и смотрим, что с поддержкой в браузерах.
Плавное появление блока, скрытого через display: none, делается на CSS двумя приёмами. Правило @starting-style задаёт стиль, с которого начинается переход, а display с ключевым словом allow-discrete в списке переходов удерживает блок на экране, пока идёт скрытие. Появление работает во всех актуальных браузерах, плавное скрытие пока только в Chrome, Edge и Safari.
Коротко
- Обычный
transitionне запускается, когда блок выходит изdisplay: none: у браузера нет стиля, от которого считать переход. @starting-styleзадаёт этот стартовый стиль. Правило работает в Chrome и Edge 117, Firefox 129 и Safari 17.5.- Строка
display 0.3s allow-discreteв списке переходов удерживает блок на экране, пока идёт скрытие. Это работает в Chrome и Edge 117 и в Safari 18, в Firefox блок исчезает сразу. - Для
popoverиdialogв список переходов добавляют ещё иoverlay. Свойство понимают только Chrome и Edge. - Высоту
autoанимируетinterpolate-size: allow-keywords, тоже только в Chrome и Edge. - В браузере без поддержки ничего не ломается: блок показывается и скрывается без анимации.
Почему transition не срабатывает с display: none
Переход в CSS считается между двумя стилями: тем, что был до изменения, и новым. Пока у блока стоит display: none, браузер его не отрисовывает, и стиля «до» у блока нет. По спецификации CSS Transitions переход для такого элемента не запускается, поэтому блок появляется сразу в конечном виде.
В примере у карточки прописан переход прозрачности, а класс is-open показывает её.
.card {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.card.is-open {
display: block;
opacity: 1;
}
Этот код проверен в Chrome 154: через 100 миллисекунд после добавления класса прозрачность уже равна 1. Со скрытием причина другая. Свойство display дискретное, промежуточных значений между block и none у него нет. Значение none применяется в тот же момент, и анимировать прозрачность уже не на чем.
Как плавно показать блок с помощью @starting-style
Правило @starting-style задаёт значения, с которых начинается переход, если до этого блок не отрисовывался. Мы записываем его внутри правила открытого состояния и указываем там стартовые значения.
.card {
display: none;
opacity: 0;
translate: 0 12px;
transition:
opacity 0.3s ease,
translate 0.3s ease;
}
.card.is-open {
display: block;
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 12px;
}
}
Когда класс is-open добавлен, браузер берёт значения из @starting-style как точку отсчёта и ведёт переход к opacity: 1, так что карточка за 0,3 секунды проявляется и сдвигается на место.
Правило @starting-style не получает дополнительного приоритета в каскаде. Если записать его отдельным блоком выше основного правила, основное правило перекроет стартовые значения. При вложенной записи, как в примере, следить за порядком не приходится.
Стартовый стиль срабатывает при каждом появлении блока, в том числе при первой отрисовке страницы. Карточка, у которой класс is-open стоит уже в разметке, тоже проявится плавно при загрузке.
Как плавно скрыть блок с помощью allow-discrete
Чтобы скрытие шло плавно, в список переходов добавляют само свойство display с ключевым словом allow-discrete. Тогда блок остаётся видимым всю длительность перехода и получает display: none только в конце.
.card {
display: none;
opacity: 0;
translate: 0 12px;
transition:
opacity 0.3s ease,
translate 0.3s ease,
display 0.3s allow-discrete;
}
.card.is-open {
display: block;
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 12px;
}
}
Слово allow-discrete разрешает переход для дискретного свойства. Обычно такие свойства переключаются на середине перехода, но для display в спецификации CSS Display сделано исключение: при переходе между none и любым другим значением блок считается видимым на всём протяжении.
Для скрытия @starting-style не нужен. Один из редакторов спецификаций CSS Таб Аткинс объяснял это в обсуждении рабочей группы (перевод наш):
«
@starting-styleсуществует потому, чтоdisplay: noneзатирает большинство стилей и не оставляет значения, от которого можно анимировать. При переходе в обратную сторону такой проблемы нет».Таб Аткинс, обсуждение в репозитории CSSWG, 14 марта 2026 года
Осталось переключать класс. Это делает короткий обработчик на JavaScript, сама анимация целиком описана в CSS.
<button class="toggle" type="button">Показать или скрыть</button>
<div class="card">Этот блок появляется и исчезает плавно.</div>
document.querySelector('.toggle').addEventListener('click', () => {
document.querySelector('.card').classList.toggle('is-open');
});
Пример проверен в Chrome 154. Через 100 миллисекунд после клика прозрачность была примерно на середине пути и при появлении, и при скрытии, а display: none блок получил только после конца перехода.
Все три фрагмента можно вставить в новый пен в Penlab: HTML, CSS и JS в свои панели. Превью обновится само, и длительность или сдвиг можно менять прямо там.
Как анимировать popover и dialog
Всплывающий блок с атрибутом popover и модальное окно dialog анимируются по той же схеме, но в список переходов добавляется свойство overlay. Такие элементы браузер рисует в верхнем слое, поверх остальной страницы. При закрытии он сразу убирает элемент из верхнего слоя, и анимация обрывается, а переход overlay откладывает это до конца анимации.
Всплывающий блок открывается без JavaScript: кнопка связана с ним атрибутом popovertarget.
<button popovertarget="hint" type="button">Открыть подсказку</button>
<div id="hint" popover>Подсказка открывается без JavaScript.</div>
[popover] {
opacity: 0;
scale: 0.95;
transition:
opacity 0.25s ease,
scale 0.25s ease,
display 0.25s allow-discrete,
overlay 0.25s allow-discrete;
}
[popover]:popover-open {
opacity: 1;
scale: 1;
@starting-style {
opacity: 0;
scale: 0.95;
}
}
Значение overlay задаёт только браузер, в своих стилях мы можем лишь включить его в transition. Сейчас свойство понимают Chrome и Edge. Остальные браузеры пропускают незнакомое имя в списке переходов, так что запись им не мешает.
Модальное окно dialog открывает метод showModal(). Само окно анимируется так же, как всплывающий блок, только открытое состояние задаёт атрибут open.
<button class="open" type="button">Открыть окно</button>
<dialog class="modal">
<p>Окно и затемнение появляются плавно.</p>
<form method="dialog">
<button>Закрыть</button>
</form>
</dialog>
.modal {
opacity: 0;
translate: 0 16px;
transition:
opacity 0.3s ease,
translate 0.3s ease,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
.modal[open] {
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 16px;
}
}
document.querySelector('.open').addEventListener('click', () => {
document.querySelector('.modal').showModal();
});
У окна есть ещё затемнение, псевдоэлемент ::backdrop. Его стартовый стиль надёжнее записать отдельным блоком: вложенная запись для псевдоэлементов заработала в браузерах позже, чем сам @starting-style.
.modal::backdrop {
background-color: rgb(0 0 0 / 0);
transition:
background-color 0.3s ease,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
.modal[open]::backdrop {
background-color: rgb(0 0 0 / 0.5);
}
@starting-style {
.modal[open]::backdrop {
background-color: rgb(0 0 0 / 0);
}
}
Оба примера, всплывающий блок и окно с затемнением, проверены в Chrome 154 и отработали в обе стороны.
Как анимировать высоту auto
Анимацию раскрытия по высоте включает свойство interpolate-size: allow-keywords. Без него браузер не умеет считать переход от числа к auto. Свойство наследуется, поэтому его ставят один раз на :root.
:root {
interpolate-size: allow-keywords;
}
.panel {
height: 0;
overflow: hidden;
visibility: hidden;
transition:
height 0.3s ease,
visibility 0.3s;
}
.panel.is-open {
height: auto;
visibility: visible;
}
Мы прячем панель через visibility: hidden, потому что блок с нулевой высотой остаётся доступным с клавиатуры и для скринридера - программы, которая читает страницу вслух. Свойство visibility браузеры анимируют давно, и во время перехода панель остаётся видимой.
Сейчас interpolate-size работает только в Chrome и Edge с версии 129, а в Firefox и Safari панель откроется и закроется без анимации высоты.
В каких браузерах это работает
На 11 октября 2026 года появление блока работает во всех актуальных браузерах, а плавное скрытие и остальные приёмы поддержаны не везде. Данные взяты из таблиц совместимости MDN и из баг-трекеров Firefox и Safari, где разработчики браузеров ведут учёт ошибок и задач. Текущие версии на эту дату: Chrome и Edge 155, Firefox 157, Safari 27.
@starting-style: Chrome и Edge 117, Firefox 129, Safari 17.5. С 6 августа 2024 года у правила статус Baseline: так помечают возможности, которые работают в свежих версиях всех основных браузеров.transition-behaviorи словоallow-discrete: Chrome и Edge 117, Firefox 129, Safari 17.4.- Переход свойства
display: Chrome и Edge 117, Safari 18. Firefox его не поддерживает, скрытие в нём происходит мгновенно. overlay: только Chrome и Edge 117.interpolate-size: только Chrome и Edge 129.- Атрибут
popover: Chrome и Edge 114, Firefox 125, Safari 17.
В Firefox работа над переходом display идёт: в версии 155 он появился, но включается настройкой layout.css.display-animations.enabled, которая по умолчанию выключена. В Safari нет overlay, поэтому при закрытии всплывающий блок или окно на время анимации теряет место в верхнем слое.
Примеры из статьи проверены только в Chrome 154. Поведение Firefox и Safari описано по документации и баг-трекерам, перед использованием в проекте его стоит проверить в этих браузерах.
Когда достаточно opacity и visibility
Старый способ с opacity и visibility работает во всех браузерах в обе стороны. Он подходит, когда скрытый блок не мешает вёрстке, например выпадающему меню с абсолютным позиционированием: такое меню и так вынуто из потока страницы.
.menu {
opacity: 0;
visibility: hidden;
transition:
opacity 0.3s ease,
visibility 0.3s;
}
.menu.is-open {
opacity: 1;
visibility: visible;
}
У остальных распространённых приёмов есть ограничения.
- Только
opacity: 0. Невидимый блок продолжает перехватывать клики. Сpointer-events: noneмышью его уже не нажать, но фокус с клавиатуры всё равно попадает на невидимые ссылки и кнопки, и скринридер читает содержимое. max-heightс запасом. Переход идёт до значения с запасом, поэтому видимая длительность не совпадает с заданной, а содержимое выше запаса обрезается.- Сдвиг за пределы экрана. Блок остаётся доступным для фокуса и скринридера.
- JavaScript с
setTimeoutили событиемtransitionend. Кода становится больше, и событие не приходит, если переход прервали.
Обычный блок, скрытый через visibility: hidden, продолжает занимать место в потоке. Когда это мешает вёрстке, нужен display: none и способ из этой статьи.
Как найти ошибку, если анимация не работает
Если переход не срабатывает, стоит проверить пять мест. Каждая ошибка из списка ниже встречается в вопросах на Stack Overflow или в баг-трекерах браузеров.
- В списке переходов нет
displayсallow-discrete: появление плавное, скрытие мгновенное. - В правиле открытого состояния нет
@starting-style: скрытие плавное, появление мгновенное. @starting-styleзаписан отдельным блоком выше основного правила, и основное правило перекрывает стартовые значения.- Слово
allow-discreteзадано отдельным свойствомtransition-behavior, и оно стоит перед сокращённой записьюtransition. Сокращённая запись сбрасывает его в значение по умолчанию. - От
@starting-styleждут анимации скрытия. Правило действует только при появлении.
По спецификации блок во время скрытия уже инертен: он виден, но на клики и фокус не реагирует. Кнопку внутри исчезающей карточки нажать не получится, и это ожидаемое поведение.
Что попробовать в редакторе
Пример проще понять, если менять его руками. На пять шагов ниже уйдёт около десяти минут.
- Откройте редактор Penlab и вставьте HTML, CSS и JS карточки из раздела про
allow-discrete. - Уберите строку
display 0.3s allow-discrete, а запятую в предыдущей строке замените на точку с запятой. Скрытие станет мгновенным. - Верните строку и запятую, а затем удалите блок
@starting-style. Теперь рывком происходит появление. - Верните блок
@starting-styleи замените сдвиг на масштаб:translate: 0 12pxнаscale: 0.95в обоих местах,translate: 0 0наscale: 1, а в списке переходовtranslate 0.3s easeнаscale 0.3s ease. - Увеличьте длительность до секунды во всех трёх строках списка переходов, иначе
displayпереключится раньше, чем закончится анимация.
Сохранённым пеном можно поделиться ссылкой или вставить его на свою страницу, об этом мы писали в заметке «Поделиться пеном и встроить его на свой сайт». Другие примеры участников собраны на витрине.
FAQ
Можно ли сделать появление блока совсем без JavaScript?
Да, если состояние переключает сам браузер. Всплывающий блок с атрибутом popover открывается кнопкой с popovertarget, блок details раскрывается по клику, а класс можно заменить селектором :has() с флажком. JavaScript нужен, когда блок показывается по событию, которого в CSS нет.
Нужен ли @starting-style для скрытия?
Нет. Правило задаёт стиль только для появления. При скрытии блок переходит к значениям своего базового правила, и для плавности достаточно display с allow-discrete в списке переходов.
Можно ли анимировать появление блока через @keyframes?
Да, можно. В ключевых кадрах display поддерживается с Chrome 116, и @starting-style с allow-discrete для анимации не нужны. Переход удобнее тем, что при быстром повторном клике разворачивается с текущего места, а анимация начинается заново. В Firefox display в ключевых кадрах не анимируется.
Как быть с пользователями, которые отключили анимацию?
Для пользователей с отключённой анимацией переход стоит убрать или сократить. Медиа-запрос prefers-reduced-motion поддерживают все актуальные браузеры.
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}
Что будет в браузере без поддержки?
Блок покажется и скроется без анимации. Правило @starting-style такой браузер пропускает, объявление transition с незнакомым словом allow-discrete отбрасывает целиком, а остальной CSS работает как обычно.
Источники
- CSS Transitions Level 2, раздел о @starting-style
- MDN: @starting-style
- Chrome for Developers: Four new CSS features for smooth entry and exit animations
- CSS Display Module Level 4, анимация display
- MDN: transition-behavior
- WebKit: возможности Safari 18.0
- CSS Transitions Level 1, запуск переходов
- MDN: overlay
- Chrome for Developers: Animate to height: auto
- Bugzilla Mozilla: переход display в Firefox
- MDN: interpolate-size
- web.dev: Baseline для анимаций появления
- WebKit Bugzilla: свойство overlay
- MDN: visibility
- Vispero: как способы скрытия влияют на доступность
- MDN: Using the Popover API
- CSS-Tricks: Using CSS Transitions on Auto Dimensions