Блог

Статьи

Плавное появление блока в 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.space/pen/g6MBdP8e

Все три фрагмента можно вставить в новый пен в 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;
  }
}
penlab.space/pen/Q9D8vjW8

Значение 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);
  }
}
penlab.space/pen/kLpOH58F

Оба примера, всплывающий блок и окно с затемнением, проверены в 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;
}
penlab.space/pen/tTnNcPsC

Мы прячем панель через 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 или в баг-трекерах браузеров.

  1. В списке переходов нет display с allow-discrete: появление плавное, скрытие мгновенное.
  2. В правиле открытого состояния нет @starting-style: скрытие плавное, появление мгновенное.
  3. @starting-style записан отдельным блоком выше основного правила, и основное правило перекрывает стартовые значения.
  4. Слово allow-discrete задано отдельным свойством transition-behavior, и оно стоит перед сокращённой записью transition. Сокращённая запись сбрасывает его в значение по умолчанию.
  5. От @starting-style ждут анимации скрытия. Правило действует только при появлении.

По спецификации блок во время скрытия уже инертен: он виден, но на клики и фокус не реагирует. Кнопку внутри исчезающей карточки нажать не получится, и это ожидаемое поведение.

Что попробовать в редакторе

Пример проще понять, если менять его руками. На пять шагов ниже уйдёт около десяти минут.

  1. Откройте редактор Penlab и вставьте HTML, CSS и JS карточки из раздела про allow-discrete.
  2. Уберите строку display 0.3s allow-discrete, а запятую в предыдущей строке замените на точку с запятой. Скрытие станет мгновенным.
  3. Верните строку и запятую, а затем удалите блок @starting-style. Теперь рывком происходит появление.
  4. Верните блок @starting-style и замените сдвиг на масштаб: translate: 0 12px на scale: 0.95 в обоих местах, translate: 0 0 на scale: 1, а в списке переходов translate 0.3s ease на scale 0.3s ease.
  5. Увеличьте длительность до секунды во всех трёх строках списка переходов, иначе 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 работает как обычно.

Источники