Хочу поделиться ещё одним способом создания css-перехода (
transition
) свойства height
от 0px
до auto
. Столкнулся с данной проблемой при разработке веб-компонентов TreeView и DataGrid. В TreeView решил сделать плавное развёртывание/свёртывание узлов, а в DataGrid — строки с дополнительным контентом. Почитав интернет, нашёл несколько способов реализации, основные — через свойство
max-height
и на javascript. Реализация на javascript была исключена — есть же css с поддержкой переходов и анимаций. Остался max-height
, тем более в примерах с выпадающими меню всё работает. В TreeView каждый узел имеет неограниченную вложенность, поэтому сразу не получится определить максимальную высоту его содержимого, да и если
max-height
задать очень большим, будут проблемы с анимацией перехода. Также, если развернуть дочерние узлы, высота родительского увеличится и может перекрыть max-height
. Как ни крути, max-height
не подходит. С DataGrid та же проблема — дополнительный контент в строке может быть любой. Нужен height:auto
! Итак, приступим к реализации перехода по свойству
height
от 0px
до auto
. Рассмотрим простой пример. Пусть:
elBlock: HTMLDivElement
— блок, который нужно развёртывать/свёртывать;elToggle: HTMLButtonElement
— кнопка-переключатель состояния.
Определим css-класс для блока, в котором установим обрезку содержимого и сам переход:
.block {
overflow: hidden;
transition: height 500ms ease;
}
Опишем обработчик события
onClick
для elToggle
:elToggle.addEventListener("click", () => {
if (elBlock.style.height === "0px") {
elBlock.style.height = `${ elBlock.scrollHeight }px`
} else {
elBlock.style.height = `${ elBlock.scrollHeight }px`;
window.getComputedStyle(elBlock, null).getPropertyValue("height");
elBlock.style.height = "0";
}
});
Осталось добавить возврат
height:auto
после перехода:elBlock.addEventListener("transitionend", () => {
if (elBlock.style.height !== "0px") {
elBlock.style.height = "auto"
}
});
Ну вот и всё, теперь развёртывание/свёртывание блока работает как надо и не зависит от размера контента.

Рисунок 1 — Пример развёртывания/свёртывания узлов в TreeView
Стоит отметить минусы данного подхода:
- использование javascript, хотелось бы только css;
- во время перехода может измениться контент (его высота,
scrollHeight
) и после его завершения, в случае возвратаauto
, высота блока резко поменяется в ту или иную сторону. Для избежания данного эффекта, необходимо отслеживать изменениеscrollHeight
и менятьheight
. Как показывает практика, обычно переходы развёртывания/свёртывания занимают по 0.5 с, а за это время пользователь вряд ли успеет изменить что-то внутри, например, в случае TreeView, развернуть дочерний узел.
Спасибо за внимание!
Комментарии (30)
vintage
12.11.2019 17:04Лайфхак — можно просто анимировать max-height с ease-in/out в зависимости от направления.
TheShock
13.11.2019 01:01Ну такой себе лайфхак. Тоже требует знания высоты, иначе будет довольно неудачная анимация.
space2pacman
16.11.2019 07:22Ничего подобного. Делаете max-height 10000px и будет плавная анимация с 0 до высоты блока и обратно. Никаких рывков и прочего. 10к конечно выглядит не супер масштабируемо но вряд ли будет меню с такой высотой.
kingjin Автор
16.11.2019 07:31Подробно, почему так не стоит делать, описано в ветке комментариев https://habr.com/ru/post/475520/#comment_20880230.
monochromer
12.11.2019 17:16Работу с
element.style.height === '0px'
я б заменил на работу с каким-нибудь классом, например,
.is-collapsed { height: 0; }
А layout-триггеры можно заменить на
requestAnimationFrame
(но с небольшим кроссбраузерным хаком):
function onSchedule(fn) { /* да, здесь два requestAnimationFrame */ requestAnimationFrame(function() { requestAnimationFrame(function() { fn(); }); }); }
Полный пример — https://jsfiddle.net/95tnmdyr/
psFitz
12.11.2019 23:25Можно без js с max-height, как написал vintage, гуглитя за 5 сек и статью писать не надо
https://stackoverflow.com/questions/3508605/how-can-i-transition-height-0-to-height-auto-using-cssPoccomaxa_zt
13.11.2019 09:42Не хочется кидать камни — но ведь автор явно в начале статьи описывал причину его отказа от `max-height`, или я не прав?..
Конечно, в 90% случаев, думаю, обычного `max-height: 5000px` вполне хватит. Но когда Вы разрабатываете переиспользуемые компоненты с четко описаными требованиями и динамическим контентом — не факт, что Вы не будете в числе 10%.TheShock
13.11.2019 21:34Конечно, в 90% случаев, думаю, обычного `max-height: 5000px` вполне хватит
И какое время анимации задавать для такого max-height? Более того, при возвращении max-height:5000 => max-height:0 — будет страшный лаг во время клика. То есть у нашего списка высота, допустим, 250 пикселей. Нажимаем кнопку, оно начинает анимироваться 0.9 секунд з max-height:5000 до max-height:250, а для пользователя это выглядит как тормоза, а потом за 0.1 секунды резко скрывается. Халтура, а не решениеvintage
13.11.2019 23:41Вы зря проигнорировали мою ремарку про анимационные функции.
TheShock
14.11.2019 14:20ease-in/ease-out? Они никак не решают этой проблемы. Может слегка уменьшают. Очень мало уменьшают
vintage
14.11.2019 15:28Во многих случаях достаточно уменьшают.
TheShock
14.11.2019 17:04Та почти во всех случаях они влияют минимально. Более того, невозможно адекватно настроить easing. Для уменьшения много=>ноль при использовании max-height не настраивается ease-in, дл увеличения — ease-out. Выглядит, как результат — отвратительно любому человеку со вкусом
TheShock
15.11.2019 21:47+1Мне вот интересны аргументы того, кто минусовал. Использование max-height обрезает или начало (в одну сторону) или конец (в другую). Соответствуенно, функции изинга, которые ответственны за начало и конец — работают некорректно
vintage
16.11.2019 01:19Да всё норм потому что: https://jsbin.com/johobicego/edit?html,css,output
TheShock
16.11.2019 01:30+1И что тут нормового? Они все открываются за разное, неконтролируемое время. ease-out нету ни на открытии, ни на закрытии (конечно, в коде есть, а визуально — нету).
Такое сырое решение подойдёт разве что в качестве заглушки: «смотрите, тут потом будет нормальная анимация закрытия, но пока и так сойдёт»vintage
16.11.2019 12:39И что тут нормового?
Можете провести опрос у пользователей и спросить норм или не норм.
Они все открываются за разное, неконтролируемое время.
Время появления любого элемента напрямую зависит от его позиции. Если вы будете открывать огромный список за то же время, что и маленький, то либо на маленьком будет слишком меееедленная анимация, либо на большом она будет еле заметная.
ease-out нету ни на открытии, ни на закрытии (конечно, в коде есть, а визуально — нету).
Оно там и не надо. Или вы считаете, что все анимации должны быть ease-in-out?
Такое сырое решение подойдёт разве что в качестве заглушки: «смотрите, тут потом будет нормальная анимация закрытия, но пока и так сойдёт»
На самом деле единственная реальная, а не вымученная проблема тут — это синхронизация нескольких анимаций соседних элементов. Но она в принципе плохо решается с нелинейными анимациями.
TheShock
16.11.2019 13:23О! Ну раз вы прикольную картинку нашли, то однозначно правы. Ведь подмена тезиса, уход в крайности, сравнивание оппонента с белками-истеричками — это ведь так умно и оригинально (на самом деле нет)
Время появления любого элемента напрямую зависит от его позиции
Хуже, что оно неконтролируемое.
Оно там и не надо. Или вы считаете, что все анимации должны быть ease-in-out?
Я считаю, что это должен решать разработчик, а не плохой технический костыль.
Я понимаю, притягивать за уши бизнес-требования к самому технически простому решению — это весело, но такая халтура подойдёт для продукта, которым пользуются три маргинала и большего от него уже не ожидается.vintage
16.11.2019 14:52Вы преувеличиваете масштаб трагедии. Из-за чего для реализации простой вещи потратите куда больше времени на написание кода и вылавливание в нём багов. Но никто ваши старания не оценит.
XAHTEP26
13.11.2019 10:55Спасибо, интересное решение. Но весь смысл статьи теряется из-за, того что вы не объясняете зачем тут вообще нужна строка:
window.getComputedStyle(elBlock, null).getPropertyValue("height");
Было бы очень полезно если бы вы это описали. Кстати эту строку можно заменить на эту:
elBlock.clientHeight;
kingjin Автор
13.11.2019 13:59Если блок развёрнут, его
height=auto
, а для перехода необходимо конкретное значение, которое получаем черезscrollHeight
. Затем нужно пересчитать («применить»)height
, для этого используетсяgetComputedStyle()
(MDN: getComputedStyle).
Да, действительно, можно использоватьclientHeight
, его вызов «применяет» текущее значениеheight
. Благодарю за совет!
Рисунок — Отличия между offsetHeight, clientHeight и scrollHeight
vital72
14.11.2019 07:57тьфу блин. я думал действительно будет рассказано про переход height от 0 до auto. а тут очевидное решение про от 0 до scrollHeight. ничего нового. зачем обманывать.
onekit
14.11.2019 12:46Никогда не понимал зачем писать рядом с нулём единицы измерения. Если скалярная величина 0, то какая нам разница чего 0. 0px? А если не указывать px, то всё пропало?
kingjin Автор
14.11.2019 12:55Разницы никакой. Если не указать
px
, сравнение (elBlock.style.height === "0px"
) не сработает, т.к. при чтении свойства возвращается"0px"
, даже если перед этим задать ему"0"
.
Shumanskaya
16.11.2019 07:19Для моих целей помогает добавление к классу .visible — padding-top/padding-bottom. Анимация применяется к ним, создавая плавное раскрытие/скрытие.
Cerberuser
Два forced layout на операцию — ну такое… Хотя сильно лучше тут, кажется, не выйдет, да (сам с подобной задачей возился полгода назад).