Height нельзя анимировать напрямую между 0 и auto - transition умеет интерполировать только числа, а auto для браузера не число, а команда "посчитай по контенту". Старое решение - max-height с заведомо большим значением, но оно тратит впустую большую часть настроенной длительности на невидимый рост. Современное - display:grid и grid-template-rows: 0fr → 1fr, который анимируется точно до реальной высоты контента, без подбора цифр на глаз.
В этом уроке
Попробуй прямо сейчас: клик по подписи "Что такое CSS Grid?" переключает чекбокс, а .accordion-toggle:checked ~ .accordion-panel меняет grid-template-rows с 0fr на 1fr - панель плавно раскрывается ровно до высоты своего текста. Смени 0.4s на 1.5s, чтобы разглядеть саму механику подробнее, или временно убери overflow:hidden у .accordion-inner - текст на миг "вылезет" наружу до того, как трек сетки успеет вырасти.
Почему height не анимируется напрямую: max-height и grid-template-rows
transition умеет плавно менять только то, что браузер способен посчитать как последовательность промежуточных числовых кадров (тема уже разбиралась в уроке про transition-property). height: auto - это не число, а инструкция "вычисли высоту по контенту", поэтому transition: height между 0 и auto просто не сработает: браузер либо мгновенно перескочит, либо анимация вообще не запустится. Старый обходной путь - max-height, у которого можно смело задать любое число, например max-height: 3000px, и оно всегда останется числом, а не auto.
.panel {
max-height: 0;
overflow: hidden;
}
.panel.open {
max-height: 3000px;
}
/* transition: max-height 1s */ - работает, но 3000px почти всегда намного больше реального контента, а значит большая часть настроенной 1s тратится впустую на невидимый рост цифры дальше вверх уже после того, как контент полностью раскрылся.
Собственная проверка (Playwright, блок с реальной высотой контента 716px, max-height: 0 → 3000px, transition 1s linear): панель достигла своей настоящей видимой высоты уже на 0.26 секунды из настроенной 1 секунды - то есть всё видимое раскрытие уложилось в первую четверть времени, а оставшиеся 74% длительности ушли на рост max-height туда, где контента уже давно нет. Отсюда и распространённая жалоба "анимация раскрытия выглядит гораздо резче, чем я задал в transition" - это не баг, а прямое следствие того, насколько щедро выбрано "запасное" число.
Современная альтернатива не требует угадывать число вообще:
.panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.4s ease;
}
.panel-inner {
overflow: hidden;
}
.panel.open {
grid-template-rows: 1fr;
} - трек сетки анимируется от 0fr до 1fr, а браузер сам вычисляет, сколько это в пикселях, исходя из реального контента внутри .panel-inner.
Собственная проверка (Playwright, два независимых блока с разной реальной высотой контента - 84px и 356px, измерено заранее через scrollHeight): после срабатывания grid-template-rows: 0fr → 1fr итоговая высота обёртки точно совпала с реальной высотой контента в обоих случаях, без единого подобранного вручную числа. Работает это благодаря тому, что браузеры умеют плавно интерполировать fr-единицы в grid-template-rows и grid-template-columns - возможность давно и надёжно поддерживается во всех актуальных браузерах (Chrome/Edge с 2022 года, Firefox ещё раньше, Safari с 2022 года).
Собираем аккордеон и меню без JavaScript
Переключать состояние "открыто/закрыто" без единой строчки JavaScript помогает чекбокс-хак: скрытый input[type="checkbox"], связанный с ним через for/id label (он же и есть видимый кликабельный триггер - клик по label переключает чекбокс), и соседний блок с контентом, который стилизуется через селектор :checked и комбинатор соседства (~ или +). Ровно та же связка label-как-триггер работает и для мыши, и для клавиатуры (label наследует фокус input), и для тач-экранов - но на тач-устройствах, как уже отмечалось в уроке про анимированные кнопки и ссылки, стоит проверять поведение отдельно от десктопного hover.
<input type="checkbox" id="q1" class="toggle">
<label for="q1" class="toggle-label">Вопрос</label>
<div class="panel">...</div>
.toggle { position: absolute; opacity: 0; }
.toggle:checked ~ .panel { grid-template-rows: 1fr; } - .toggle:checked ~ .panel находит панель по признаку "соседний элемент после отмеченного чекбокса", не завязываясь на JavaScript.
.toggle:checked .panel { grid-template-rows: 1fr; } - без ~ или + браузер ищет .panel ВНУТРИ чекбокса (а внутри пустого input вложенных элементов нет), правило никогда не сработает.
Ровно та же связка чекбокс + label + :checked ~ работает и для выпадающего меню - разница только в контенте панели (список ссылок вместо текста ответа) и в том, что чекбокс здесь удобнее спрятать за иконкой-гамбургером через сам label, а не через видимый текст.
Важная оговорка про доступность: чекбокс-хак решает задачу визуально, но не идеально семантически. Экранный диктор объявит скрытый input как обычный чекбокс - "отмечено" или "не отмечено", а не как "развёрнуто" или "свёрнуто" для секции. Там, где нужна безупречная встроенная семантика без единого атрибута ARIA, у HTML уже есть готовый инструмент - теги <details> и <summary>, браузер сам объявляет диктору правильное состояние. Чекбокс-хак остаётся рабочим и распространённым паттерном именно там, где нужна кастомная анимация раскрытия, которую голый <details> долгое время не умел красиво анимировать при закрытии.
Частые ошибки новичков
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Писать transition: height 0.3s и менять height: auto ↔ height: 0 | Анимация не запускается или срабатывает мгновенным скачком - auto не число | Использовать max-height с фиксированным числом или grid-template-rows: 0fr → 1fr |
| Брать max-height впритык к реальной высоте контента (например 200px для блока на 190px) | При малейшем увеличении текста (перевод, смена шрифта, добавленный пункт) низ панели обрезается | Либо брать max-height с запасом минимум 2x, либо сразу перейти на grid-template-rows |
| Брать max-height с огромным запасом "на всякий случай" (например 3000px для блока на 200px) | Большая часть настроенной длительности transition тратится на невидимый рост - раскрытие выглядит резче задуманного | Использовать grid-template-rows: 0fr → 1fr - он анимируется ровно до реальной высоты контента |
| Забыть overflow: hidden на внутреннем блоке с контентом при grid-template-rows | Пока трек сетки ещё сжат (0fr), текст всё равно видно - он "вылезает" наружу поверх соседних блоков | Всегда оборачивать контент в div с overflow: hidden внутри трека grid-template-rows |
| Стилизовать панель через .toggle:checked .panel (потомок) вместо .toggle:checked ~ .panel (сосед) | Правило не срабатывает - внутри пустого input вложенных элементов нет | Использовать комбинатор соседства ~ или + между чекбоксом и панелью |
Шпаргалка
| Запись | Что означает |
|---|---|
| max-height: 0 → Npx + transition | Старая техника, требует угадывать число, тратит время анимации впустую |
| display:grid; grid-template-rows: 0fr → 1fr | Современная техника - анимируется точно до реальной высоты контента |
| overflow: hidden на внутреннем блоке | Обязателен для grid-template-rows - прячет контент, пока трек сжат |
| input[type=checkbox] + label[for] + :checked ~ | Переключение открыто/закрыто без единой строки JavaScript |
| <details><summary> | Нативная альтернатива с готовой доступностью, но менее гибкая в анимации |
Коротко: отвечаем на вопросы
Почему нельзя просто написать transition: height 0.3s и анимировать открытие блока?
transition умеет плавно менять только числовые значения, которые браузер может пересчитывать кадр за кадром. height: auto - это не число, а инструкция "вычисли высоту по контенту", поэтому переход между auto и 0 не анимируется: браузер либо переключает мгновенно, либо анимация вовсе не запускается. Нужен либо max-height с конкретным числом, либо grid-template-rows: 0fr → 1fr.
Правда ли, что max-height с большим "запасным" значением полностью решает проблему анимации высоты?
Только визуально, но не по длительности. Собственная проверка на блоке с реальной высотой контента 716px и max-height: 0 → 3000px (transition 1s linear) показала, что панель достигает настоящей видимой высоты уже на 0.26 секунды из настроенной секунды - то есть около 74% заданной длительности уходит на невидимый рост max-height дальше вверх, где контента уже нет. Раскрытие визуально выглядит намного резче, чем задано в transition.
Чем grid-template-rows: 0fr → 1fr лучше max-height для анимации раскрытия?
Не нужно угадывать число. Собственная проверка на двух блоках с разной реальной высотой контента (84px и 356px) показала, что после срабатывания grid-template-rows итоговая высота обёртки точно совпадает с реальной высотой контента в обоих случаях - браузер сам вычисляет нужное значение в пикселях, интерполируя fr-единицы, а не растягивая заранее подобранное число.
Доступен ли аккордеон на чекбоксах для пользователей с экранным диктором?
Не полностью. Диктор объявляет скрытый input как обычный чекбокс - "отмечено" или "не отмечено", а не как "развёрнуто"/"свёрнуто" для секции, как это делает нативный тег details. Чекбокс-хак остаётся рабочим и распространённым решением там, где важна именно кастомная CSS-анимация раскрытия; там, где важнее готовая семантика "из коробки", стоит рассмотреть <details><summary>.