Аккордеон, выпадающее меню, блок «Показать ещё» — всё это элементы, которые должны плавно раскрываться до высоты своего содержимого. Казалось бы, хватит transition: height и смены height: 0 на height: auto. На деле блок просто прыгает из одного состояния в другое, и анимации нет.
Годами это обходили хаками. Теперь в CSS есть два инструмента, которые решают задачу напрямую: свойство interpolate-size и функция calc-size(). Разберём оба, посмотрим, где они не помогут, и соберём запасной вариант для браузеров, которые их пока не знают.
Почему height: auto не анимировался
Transitions в CSS работают так: браузер берёт начальное и конечное значения и на каждом кадре вычисляет промежуточное. Между 0 и 200px посчитать середину легко. А auto — не число, а ключевое слово: «сколько потребует содержимое». Точку посередине между нулём и словом браузер не вычисляет, поэтому значение меняется сразу.
Самый популярный обход — анимировать max-height до заведомо большого числа:
.dropdown {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s;
}
.dropdown.is-open {
max-height: 1000px; /* «с запасом» */
}
У приёма две проблемы. Время перехода растягивается на все 1000 пикселей, а реальный контент занимает, скажем, 150. Поэтому раскрытие выглядит слишком резким, а закрытие начинается с паузы: браузер сначала «проматывает» пустые 850 пикселей. И если контент однажды окажется выше 1000 пикселей, он обрежется.
Второй обход — измерить высоту скриптом и подставить её числом:
const open = (element) => {
element.style.height = `${element.scrollHeight}px`;
element.addEventListener('transitionend', () => {
element.style.height = 'auto';
}, { once: true });
};
Работает точно, но это код, который нужно поддерживать: учитывать ресайз окна, подгрузку картинок внутри и обратную анимацию при закрытии.
Одна строка на весь сайт: interpolate-size
Свойство interpolate-size разрешает браузеру интерполировать, то есть вычислять промежуточные значения, между числом и ключевым словом размера. У него два значения:
- numeric-only — поведение по умолчанию, анимируются только числа;
- allow-keywords — анимируются и переходы к ключевым словам вроде auto.
Свойство наследуется, поэтому его достаточно объявить один раз на корневом элементе — и все потомки получат анимацию размеров по ключевым словам:
:root {
interpolate-size: allow-keywords;
}
.panel {
height: 0;
overflow: clip;
transition: height 0.4s ease;
}
.panel.is-open {
height: auto;
}
Больше ничего не нужно: панель плавно раскрывается до высоты содержимого и так же плавно схлопывается обратно. Сколько текста окажется внутри, неважно — браузер сам вычисляет конечную высоту на старте перехода.
Кроме transition, так же работают и @keyframes. Например, можно добавить небольшой «отскок» при раскрытии:
@keyframes reveal {
0% { height: 0; }
80% { height: calc-size(auto, size + 12px); }
100% { height: auto; }
}
.panel.is-open {
height: auto;
animation: reveal 0.5s ease-out;
}
Функцию calc-size() из этого примера разберём ниже: здесь она даёт перелёт на 12 пикселей выше итоговой высоты.
Не только высота
Анимироваться могут переходы к любому из ключевых слов, которые описывают размер по содержимому: auto, min-content, max-content и fit-content (у flex-basis к ним добавляется ещё content). Чем отличаются последние три, подробно разобрано в статье про min-content, max-content и fit-content.
Пример с шириной: круглый значок, который при наведении раскрывается в плашку с текстом. Конечная ширина зависит от длины надписи, но считать её не нужно:
.badge {
width: 44px;
overflow: clip;
white-space: nowrap;
transition: width 0.35s ease;
}
.badge:hover,
.badge:focus-visible {
width: fit-content;
}
details без скрипта, но с анимацией
Элементы details и summary — готовый аккордеон без единой строки JavaScript, а общий атрибут name собирает несколько блоков в группу, где открыт только один. Подробнее о таких приёмах — в статье о том, что CSS теперь умеет без JavaScript. Долго не удавалось одно: анимировать раскрытие. Содержимое details появлялось мгновенно.
Теперь раскрываемая часть доступна через псевдоэлемент ::details-content, и ей можно задать transition по высоте. Есть нюанс: браузер прячет содержимое закрытого details через content-visibility: hidden. Это свойство дискретное: у него нет промежуточных значений, оно переключается мгновенно, и закрытие оборвалось бы на первом кадре. Значение allow-discrete в переходе откладывает переключение до конца анимации:
:root {
interpolate-size: allow-keywords;
}
details::details-content {
height: 0;
overflow: clip;
transition:
height 0.4s ease,
content-visibility 0.4s allow-discrete;
}
details[open]::details-content {
height: auto;
}
Сам ::details-content уже поддерживается во всех основных браузерах, то есть оформить раскрываемую часть (фон, отступы) можно везде. А вот анимация высоты из примера сработает только там, где есть interpolate-size.
calc-size(): когда нужна арифметика
Обычная calc() работает только с числами: написать calc(auto + 20px) нельзя. Функция calc-size() закрывает этот пробел. Она принимает два аргумента:
- ключевое слово, от которого считаем: auto, min-content, max-content, fit-content;
- выражение, в котором слово size означает размер, вычисленный по первому аргументу.
/* ровно размер по содержимому — то же, что просто auto */
height: calc-size(auto, size);
/* по содержимому плюс запас */
height: calc-size(auto, size + 2rem);
width: calc-size(max-content, size - 40px);
/* половина ширины по содержимому */
width: calc-size(fit-content, size / 2);
/* внутри доступны и математические функции: округляем вверх до шага 50px */
height: calc-size(auto, round(up, size, 50px));
Главное следствие: значение с calc-size() анимируется само по себе, даже без interpolate-size. То есть панель из первого раздела можно раскрыть и так, точечно:
.panel.is-open {
height: auto; /* для браузеров без calc-size() */
height: calc-size(auto, size);
}
Первая строка нужна для браузеров, которые calc-size() не понимают: неизвестное значение они проигнорируют и возьмут auto. Если задача — просто разрешить анимацию до auto, удобнее один раз включить interpolate-size на корне. А calc-size() оставить для случаев, где к размеру по содержимому действительно нужно что-то прибавить или от него отнять. Подробнее о самой calc() — в мануале про свойство calc().
Пример такого случая: превью картинки, которое по клику плавно вырастает до половины натурального размера. Ключевое слово size у картинки — её собственный размер из файла. Управляем высотой, а ширина подстраивается под пропорции:
.photo img {
width: auto;
height: 80px;
transition: height 0.4s ease;
}
.photo.is-zoomed img {
height: calc-size(auto, size / 2);
}
Второй пример — кнопка, которая при наведении расширяется ровно на ширину выезжающей стрелки. Стрелка вынесена из потока через position: absolute, поэтому к собственной ширине кнопки нужно прибавить место под неё. Кнопки с текстом разной длины получат каждая свою ширину плюс одинаковые 28 пикселей:
.cta {
position: relative;
width: auto;
overflow: clip;
transition: width 0.3s ease;
}
.cta:hover,
.cta:focus-visible {
width: calc-size(auto, size + 28px);
}
Где не сработает
Между двумя ключевыми словами
Одна из сторон перехода обязательно должна быть числом или calc-size() от того же ключевого слова. Переход от auto к fit-content или от min-content к max-content не анимируется ни с interpolate-size, ни с calc-size():
/* на :root включён interpolate-size: allow-keywords */
.tag { width: min-content; transition: width 0.3s; }
.tag:hover { width: max-content; } /* прыжок, анимации не будет */
.tag-2 { width: 3rem; transition: width 0.3s; }
.tag-2:hover { width: max-content; } /* плавно: число → ключевое слово */
За пределами свойств размера
Оба инструмента работают только со свойствами, которые задают размер блока: width, height, их логические пары inline-size и block-size, варианты с min- и max-, а также flex-basis. Например, центрирование через margin: auto так не анимировать: auto в отступе означает «раздели свободное место», а не размер по содержимому. Запись вида margin: calc-size(auto, size) браузер считает ошибкой и отбрасывает всё объявление.
В старом проекте без проверки
Значение allow-keywords не сделали поведением по умолчанию как раз ради совместимости: где-то высоту уже анимирует JavaScript, а где-то вёрстка опирается на то, что переход к auto происходит мгновенно. В новый проект строку на :root можно смело добавить в базовые стили. В существующий — сначала включить на конкретных компонентах и проверить, что ничего не поехало.
Свёрнутый блок не спрятан по-настоящему
Блок с height: 0 и overflow: clip невидим глазу, но не клавиатуре и не скринридеру. Ссылки и кнопки внутри по-прежнему получают фокус по Tab, а программа экранного доступа зачитает весь текст. Простое решение — добавить к свёрнутому состоянию visibility: hidden. У visibility есть особенность: при переходе между visible и hidden элемент остаётся видимым всю анимацию и прячется только в её конце. Поэтому закрытие не обрывается:
.panel {
height: 0;
overflow: clip;
visibility: hidden;
transition: height 0.4s ease, visibility 0.4s;
}
.panel.is-open {
height: auto;
visibility: visible;
}
У кнопки-переключателя стоит обновлять aria-expanded, чтобы скринридер объявлял, раскрыта панель или нет. С details об этом заботиться не нужно: браузер прячет и озвучивает его содержимое сам.
Поддержка браузерами и запасной вариант
interpolate-size и calc-size() пока есть только в браузерах на движке Chromium:
Для interpolate-size это не страшно: это прогрессивное улучшение в чистом виде. Браузер без поддержки проигнорирует незнакомое свойство, и блок будет раскрываться так же, как раньше, — сразу, без анимации. Ничего не ломается, просто часть пользователей не увидит плавности.
С calc-size() нужно подумать отдельно. Если добавка к размеру декоративная, как место под стрелку в кнопке, её отсутствие ничего не испортит. Но если от неё зависит раскладка, нужен запасной вариант — объявление выше по коду (как height: auto в примере из прошлого раздела) или отдельная ветка в директиве @supports:
@supports not (height: calc-size(auto, size)) {
.cta:hover,
.cta:focus-visible {
padding-inline-end: 48px; /* место под стрелку без анимации ширины */
}
}
Плавно везде: трюк с grid-template-rows
Если анимация раскрытия нужна во всех браузерах уже сейчас, есть проверенный обходной путь через CSS Grid. Блок делают сеткой из одной строки и анимируют не высоту, а размер этой строки: от 0fr до 1fr. Доли (fr) — это числа, их браузеры интерполировать умеют, а 1fr в такой сетке как раз равен высоте содержимого:
.collapse {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.4s ease;
}
.collapse.is-open {
grid-template-rows: 1fr;
}
.collapse__inner {
min-height: 0; /* иначе строка не сожмётся меньше содержимого */
overflow: hidden;
}
Цена — лишняя обёртка в разметке и display: grid на контейнере, который, возможно, был нужен для другой раскладки. Поэтому для новых проектов логично держать в голове оба способа: interpolate-size как основной, а grid-трюк — там, где плавность во всех браузерах обязательна.
Уважаем настройки пользователя
Любые анимации раскрытия стоит отключать для тех, кто попросил систему уменьшить движение на экране. Для этого есть медиазапрос prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
.panel,
details::details-content {
transition: none;
}
}
Шпаргалка
| Задача | Что взять |
| Плавно раскрывать блоки до auto по всему сайту | interpolate-size: allow-keywords на :root |
| Анимировать один конкретный элемент | calc-size(auto, size) в конечном состоянии |
| Размер по содержимому плюс-минус что-то | calc-size(auto, size + 28px) и другие выражения с size |
| Анимированный аккордеон без JavaScript | details + ::details-content + content-visibility с allow-discrete |
| Плавность обязательна во всех браузерах | grid-template-rows: 0fr → 1fr |
| Скрыть содержимое свёрнутого блока от клавиатуры и скринридеров | visibility: hidden в свёрнутом состоянии |
Главное, что стоит запомнить: строка interpolate-size: allow-keywords ничего не ломает там, где её не понимают, поэтому её можно ставить в базовые стили новых проектов уже сейчас. В Chromium раскрытия станут плавными, в остальных браузерах всё будет работать как прежде.
Комментарии (0)