Do you need to animate a transition of a CSS property? No problem... right?
Recently I decided I wanted to code an accordion component without any libraries or frameworks. Initially I thought I would just animate the height of each section of the accordion using the transition property in CSS. That code would look something like this:
<div class="accordion">
<div class="item current-item">
<div class="header">Header Text</div>
<div class="body">
<div class="body-text">Sample text</div>
</div>
</div>
</div>
.item .body {
height: 0;
overflow: hidden;
transition: 1s ease height;
}
.item.current-item .body {
height: auto;
}
var headers = document.querySelectorAll('.header');
for (var i=0; i<headers.length; i++) {
headers[i].addEventListener('click', toggleDisplay);
}
function toggleDisplay() {
if (this.parentNode.classList.contains('current-item')) {
var currentlyDisplayed = document.querySelectorAll('.current-item');
for (var e=0; e<currentlyDisplayed.length; e++) {
currentlyDisplayed[e].classList.remove('current-item');
}
} else {
this.closest('.item').classList.add('current-item');
}
}
Unfortunately the change in height appears instantly and no animation occurs. This happens because the actual value of auto is not calculated at the time the transition is applied. In other words, the animation doesn't know what height it's animating to until it gets there.
This StackOverflow post has 47 answers where users have submitted different approaches to accomplishing this. So where do I start?
Solution 1: Animate the transition to a fixed height
By dropping the value of auto, the transition in height can be animated from 0px to a fixed value, 150px for example. For this example, keep the same HTML and JavaScript as above, but using the following CSS:
.item .body {
height: 0;
overflow: hidden;
transition: 1s ease height;
}
.item.current-item .body {
height: 150px;
}
DEMO
In this example, the transition between a height of 0px and 150px is smooth, but because the second item in the accordion has more body text than the first item, displaying all of the content of that second item becomes a problem. When the content extends the section larger than our hardcoded value of 150px the content is cropped. So that's not going to work...
Solution 2: max-height: 9999px
Moving on from height, using a max-height of 9999px, or something that would never be reached, will solve both the problem of displaying the transition smoothly and the problem of cropping containers that vary in height.
For this example, keep the same HTML and JavaScript as above, but use the following CSS:
.item .body {
max-height: 0;
overflow: hidden;
transition: 1s ease max-height;
}
.item.current-item .body {
max-height: 9999px;
}
DEMO
This works in a dynamic situation where a fixed height can't be used. But the problem here, if you haven't already noticed it, is a UI response delay occurring right before the transition occurs. Maybe this works for you, but at this point achieving a smooth accordion became a challenge I couldn't give up.
Solution 3: Use offsetHeight()
By finding the offsetHeight() inside of the toggleDisplay() function I found I was able to animate the height property and avoid any delay. This is ultimately accomplished by setting an inline style for the height to the offsetHeight() of that section. This inline style is also reapplied every time the window is resized in order to remain compatible with responsive layouts.
The final product:
<div class="accordion" id="page-info-1">
<div class="item current-item">
<div class="header">Header Text</div>
<div class="body">
<div class="body-text">Sample text</div>
</div>
</div>
<div class="item">
<div class="header">Header Text</div>
<div class="body">
<div class="body-text">Sample text</div>
</div>
</div>
</div>
.item .body {
height: 0;
overflow: hidden;
transition: 1s ease height;
}
function accordion(accordionId) {
var headers = document.querySelectorAll('#' + accordionId + ' .header');
for (var i=0; i<headers.length; i++) {
headers[i].addEventListener('click', function() {
var parentId = this.parentNode.parentNode.id;
if (this.parentNode.classList.contains('current-item')) {
clearAll(parentId);
} else {
clearAll(parentId);
this.closest('.item').classList.add('current-item');
var sectionHeight = document.querySelectorAll('#' + parentId + ' .current-item .body-text')[0].offsetHeight;
document.querySelectorAll('#' + parentId + ' .current-item .body')[0].setAttribute('style', 'height: ' + sectionHeight + 'px');
}
});
var clearAll = function (selector) {
var currentlyDisplayed = document.querySelectorAll('#' + selector + ' .current-item');
var currentSection = document.querySelectorAll('#' + selector + ' .current-item .body')[0];
for (var e=0; e<currentlyDisplayed.length; e++) {
currentlyDisplayed[e].classList.remove('current-item');
currentSection.setAttribute('style', 'height: 0px');
}
};
var resizeAccordion = function () {
var accordions = document.getElementsByClassName('accordion');
for (var a = 0; a<accordions.length; a++) {
var newHeight = document.querySelectorAll('.accordion .current-item .body-text')[a].offsetHeight;
document.querySelectorAll('.accordion .current-item .body')[a].setAttribute('style', 'height: ' + newHeight + 'px');
}
};
resizeAccordion();
window.onresize = resizeAccordion;
}
}
accordion('page-info-1');
DEMO
That's it! For the HTML, CSS, and JavaScript used in the final demo, check out the repo on Github:
https://github.com/andybeckmann/AB_accordion