아코디언이나 "더 보기" 패널을 만들 때마다 걸리는 게 있다. height: 0에서 height: auto로 트랜지션을 걸면 애니메이션이 안 먹는다. auto는 계산된 길이 값이 아니라서 브라우저가 중간값을 못 만든다. 그래서 다들 max-height: 1000px 같은 꼼수를 써왔다.
근데 이 방식은 문제가 많다. 값을 실제 콘텐츠보다 크게 잡아야 하니 애니메이션 타이밍이 어긋난다. 콘텐츠가 300px인데 max-height를 1000px로 두면, 닫을 때 앞부분 700px 구간은 아무 변화 없이 시간만 잡아먹는다. 그렇다고 JS로 scrollHeight를 읽어서 픽셀 값을 박아넣으면 코드가 지저분해지고 리사이즈 대응도 따로 해줘야 한다.
interpolate-size 한 줄
이제 CSS만으로 된다. 핵심은 interpolate-size: allow-keywords다. 이걸 켜면 auto, min-content, fit-content 같은 키워드도 트랜지션 대상이 된다.
:root {
interpolate-size: allow-keywords;
}
.panel {
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
.panel.open {
height: auto;
}
:root에 한 번 선언하면 하위 요소 전체에 상속된다. max-height도, JS도 없이 콘텐츠 높이 그대로 부드럽게 열리고 닫힌다.
calc-size()로 국소 적용
전역으로 켜기가 부담스럽거나 특정 요소만 필요하면 calc-size()를 쓴다. 계산할 수 있는 키워드를 명시적으로 값에 넣는 방식이라 interpolate-size 선언 없이도 동작한다.
.panel {
height: 0;
transition: height 0.3s ease;
}
.panel.open {
height: calc-size(auto, size);
}
calc-size(auto, size)는 "auto를 계산해서 그 값을 그대로 써라"는 뜻이다. 여기에 연산도 넣을 수 있어서 calc-size(auto, size + 2rem)처럼 여백을 더해 열 수도 있다.
details 요소랑 같이
가장 잘 어울리는 조합은 네이티브 <details>다. 열림 상태 관리를 브라우저가 해주니 클래스 토글조차 필요 없다.
details::details-content {
height: 0;
overflow: hidden;
transition: height 0.3s ease, content-visibility 0.3s allow-discrete;
}
details[open]::details-content {
height: calc-size(auto, size);
}
::details-content 가상 요소가 펼쳐지는 내용 전체를 가리킨다. content-visibility를 allow-discrete로 같이 트랜지션하면 닫힐 때 콘텐츠가 툭 사라지지 않고 높이 애니메이션이 끝난 뒤 정리된다.
정리
Chrome 129+, Safari 26에서 쓸 수 있고 아직 Firefox는 준비 중이다. 그래서 트랜지션은 어디까지나 향상 요소로 두는 게 안전하다. 미지원 브라우저에서는 애니메이션 없이 즉시 열릴 뿐, 기능 자체는 멀쩡히 동작하니 점진적 향상으로 붙이기 딱 좋다. max-height 꼼수와 JS 높이 계산을 둘 다 걷어낼 수 있다는 것만으로도 충분히 반갑다.