12k
All articles

Как анимировать display: none без JavaScript-хаков

Анимируйте display none на CSS с allow-discrete и @starting-style, а также обработку overlay для dialog и popover и поддержку браузеров.

OpenReplay Team
OpenReplay Team
Как анимировать display: none без JavaScript-хаков

Чтобы анимировать элемент при переходе к display: none или от него средствами одного лишь CSS, добавьте display в transition с ключевым словом allow-discrete и укажите начальное состояние в правиле @starting-style — никакого JavaScript, никаких setTimeout, никаких обработчиков transitionend. Для этого были реализованы два механизма: transition-behavior: allow-discrete позволяет дискретным свойствам, таким как display, участвовать в переходе, а @starting-style предоставляет браузеру состояние «до открытия», из которого будет выполняться анимация. В этом руководстве вы найдёте готовые к использованию рецепты для анимации появления и скрытия элементов, разбор случая с диалогами и поповерами в верхнем слое с его особенностью overlay, а также честную оценку текущей поддержки в браузерах.

Ключевые выводы

  • Анимируйте display, включив его в сокращённое свойство transition с allow-discrete, и определите начальное состояние в блоке @starting-style — JavaScript не требуется.
  • display — дискретное свойство: при использовании allow-discrete браузер переключает его в видимое состояние в 0% при появлении и в none в 100% при скрытии, благодаря чему контент остаётся видимым на протяжении всей анимации.
  • Если вы используете отдельное объявление transition-behavior: allow-discrete, оно должно идти после сокращённого свойства transition, иначе браузер его проигнорирует.
  • Для диалогов и поповеров также добавляйте overlay ... allow-discrete в transition, но рассматривайте это как прогрессивное улучшение, поскольку overlay пока не входит в Baseline.
  • @starting-style и allow-discrete входят в Baseline Newly Available начиная с выхода Firefox 129 6 августа 2024 года; в браузерах без поддержки этих функций элемент будет просто мгновенно появляться и исчезать.

Почему display: none нельзя было анимировать

CSS-переход требует стиля «до изменения», из которого будет выполняться анимация. Элемент с display: none — или только что вставленный в DOM — не имеет предыдущего отрисованного состояния, поэтому исторически браузеру не от чего было интерполировать, и переход никогда не срабатывал. Именно поэтому разработчики прибегали к переключению классов через setTimeout или к хакам с двойным requestAnimationFrame для принудительного перерасчёта макета.

Распространённый обходной путь — затухание через opacity: 0 с оставлением элемента в DOM — не является полноценной заменой. opacity: 0 оставляет элемент в потоке документа, в порядке обхода по Tab и в дереве доступности, так что пользователи клавиатуры и программ чтения с экрана по-прежнему могут попасть на контент, который визуально выглядит скрытым. display: none удаляет элемент из всех трёх контекстов. На практике записи сессий в частично мигрированных компонентах наглядно демонстрируют именно этот класс ошибок: пользователь нажимает Tab или кликает на панель, которая выглядит закрытой, но была лишь скрыта через opacity и никогда не удалена из потока документа. Анимация настоящего display: none полностью устраняет эту проблему.

Два CSS-механизма, решающих проблему: allow-discrete и @starting-style

display — это дискретное свойство: оно не может интерполировать значения, это переключатель вкл/выкл. Дискретно анимируемые свойства, как правило, переключаются между двумя значениями на отметке 50% анимации; исключение составляет анимация перехода к display: none или content-visibility: hidden и от них — в этом случае браузер переключает значения таким образом, чтобы анимируемый контент оставался видимым на протяжении всей анимации. Направление имеет значение: при анимации display из none в block значение переключается в block на 0% длительности, чтобы элемент был виден на всём протяжении; при анимации из block в none — переключается в none на 100% длительности, чтобы элемент также оставался виден на всём протяжении. Именно это обеспечивает видимость затухания в обоих направлениях.

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

Рецепт скрытия: переход к display: none

Чтобы плавно скрыть элемент и затем удалить его из потока документа, включите display в transition с allow-discrete и задайте скрытое состояние:

.panel {
  opacity: 1;
  transition: opacity 0.3s ease, display 0.3s allow-discrete;
}

.panel.is-hidden {
  opacity: 0;
  display: none;
}

Opacity анимируется до 0 за 300 мс; display сохраняет видимое значение до отметки 100%, после чего переключается в none. Если забыть указать allow-discrete для display, элемент исчезнет мгновенно — это самая распространённая ошибка при использовании данной техники.

Рецепт появления: анимация из display: none

Для элемента, который изначально скрыт, поместите значения «до открытия» в правило @starting-style. При использовании вложенности CSS всё это можно разместить в одном блоке:

.panel {
  display: none;
  opacity: 0;
  transition: opacity 0.3s ease, display 0.3s allow-discrete;
}

.panel.is-open {
  display: block;
  opacity: 1;

  @starting-style {
    opacity: 0;
  }
}

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

.panel {
  transition: opacity 0.3s, display 0.3s;
  transition-behavior: allow-discrete; /* должно идти ПОСЛЕ сокращённого свойства */
}

Для поддержки старых движков паттерн MDN для кросс-браузерной совместимости предполагает двойное объявление transition — первый экземпляр без allow-discrete обеспечивает кросс-браузерную поддержку, гарантируя анимацию остальных свойств в браузерах без поддержки transition-behavior.

Диалоги и поповеры: особенность с overlay

Элементы верхнего слоя — <dialog> и всё, что использует атрибут popover, — представляют наиболее ценный сценарий использования, и для них есть дополнительное требование. Для диалогов и поповеров необходимо также добавить overlay ... allow-discrete в transition, иначе элемент мгновенно покидает верхний слой и анимация выхода никогда не отображается:

dialog {
  translate: 0 100vh;
  transition:
    translate 0.4s ease-out,
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete;
}

dialog[open] {
  translate: 0 0;

  @starting-style {
    translate: 0 100vh;
  }
}

Для поповера замените dialog[open] на псевдокласс :popover-open. Свойство overlay откладывает удаление из верхнего слоя: оно гарантирует, что элемент не будет удалён из верхнего слоя до завершения анимации; в более сложных случаях без этого элемент может быть удалён из оверлея слишком быстро, что делает анимацию несвязной или неэффективной.

Важная оговорка, которую конкуренты обходят стороной: overlay не входит в Baseline. MDN помечает его как экспериментальный — эта функция не является частью Baseline, поскольку не работает в некоторых наиболее широко используемых браузерах. Рассматривайте её как прогрессивное улучшение; там, где она не поддерживается, диалог по-прежнему будет открываться и закрываться, просто без отложенного выхода.

Поддержка браузерами и деградация

@starting-style и transition-behavior: allow-discrete оба вошли в Baseline Newly Available с выходом Firefox 129, выпущенного 6 августа 2024 года. Однако для анимации самого display недостаточно только этих двух функций. Это работает в Chrome и Edge 117+, а также в Safari 18+ — Safari 17.4 добавил transition-behavior, а 17.5 добавил @starting-style, но анимация display с их помощью работает только начиная с Safari 18. Firefox 129+ поддерживает обе функции, однако по состоянию на середину 2026 года он по-прежнему не анимирует свойство display, поэтому в Firefox элемент просто мгновенно появляется и скрывается. Использование display в @keyframes поддерживается начиная с Chrome 116.

ФункцияСтатусЗапасной вариант
transition-behavior: allow-discreteBaseline (авг. 2024)Мгновенное появление/скрытие
@starting-styleBaseline (авг. 2024)Отсутствие анимации появления
overlayНе входит в BaselineДиалог всё равно открывается/закрывается

Это чистое прогрессивное улучшение. В браузерах без поддержки этих функций элементы, анимируемые при появлении в верхнем слое или из состояния display: none, будут просто появляться на странице без перехода — так же, как сегодня. Никаких полифилов, никаких JavaScript-запасных вариантов. При необходимости явной границы используйте определение функций:

@supports (transition-behavior: allow-discrete) {
  /* современные анимации появления/скрытия */
}

Когда использовать View Transitions вместо этого подхода

Используйте описанные переходы при переключении видимости существующего элемента. Обращайтесь к View Transitions API, когда добавляете или удаляете DOM-узлы. Переходы в рамках одного документа вошли в Baseline Newly Available 14 октября 2025 года — одновременно с выходом Firefox 144 — и поддерживаются в Chrome 111+, Edge 111+, Safari 18+ и Firefox 144+. Оберните мутацию DOM в document.startViewTransition() с простым запасным вариантом для неподдерживаемых движков:

if (document.startViewTransition) {
  document.startViewTransition(() => card.remove());
} else {
  card.remove();
}

Современный подход избавляет от старой JavaScript-хореографии: включите display в transition с allow-discrete, определите начальное состояние в @starting-style, добавьте overlay для элементов верхнего слоя и позвольте неподдерживаемым браузерам переключаться мгновенно. Избавьтесь от setTimeout и используйте CSS.

Часто задаваемые вопросы

Почему анимация выхода не срабатывает, даже после того как я добавил allow-discrete к переходу display?

Наиболее распространённая причина — отдельное объявление transition-behavior: allow-discrete стоит перед сокращённым свойством transition, из-за чего браузер молча его игнорирует. При отдельном объявлении transition-behavior должно идти после сокращённого свойства transition, иначе сокращённое свойство сбросит его. Если вы указываете allow-discrete непосредственно внутри значения transition, порядок внутри сокращённого свойства не важен и эта ловушка не применяется.

Нужен ли @starting-style, если я хочу только плавно скрыть элемент с display: none?

Нет. @starting-style требуется только для анимации появления — когда элемент переходит из display: none или только что вставлен в DOM и нуждается в состоянии «до открытия» для анимации. Для чистого скрытия — анимации уже видимого элемента до display: none — достаточно включить display в transition с allow-discrete и задать скрытое состояние. Добавляйте @starting-style только тогда, когда также анимируете появление элемента.

Что происходит в браузерах без поддержки transition-behavior или @starting-style?

Элемент просто появляется и исчезает мгновенно — точно так же, как без какой-либо анимации. Это прогрессивное улучшение, поэтому полифилы и JavaScript-запасные варианты не нужны. Для диалогов и поповеров браузеры без поддержки не входящего в Baseline свойства overlay по-прежнему корректно открывают и закрывают элемент — просто без отложенного выхода из верхнего слоя. При необходимости явной границы можно использовать правило @supports (transition-behavior: allow-discrete).

Когда следует использовать View Transitions API вместо анимации display: none?

Используйте View Transitions при добавлении или удалении DOM-узлов, а переходы с display — при переключении видимости элемента, уже существующего в DOM. Переходы в рамках одного документа вошли в Baseline Newly Available 14 октября 2025 года и поддерживаются в Chrome 111+, Edge 111+, Safari 18+ и Firefox 144+. Оберните мутацию DOM в document.startViewTransition() с простым запасным вариантом для неподдерживаемых движков.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.