In this snippet below, I’m trying to apply a slide down functionality to each panel when opened. The result is that slide down is working.
The problem is…
In mobile, when a second panel is opened, the first panel remains at full height.
Slide up functionality isn’t working as expected.
Lastly, I have hundreds of these accordions on my site. I need to be able to update the JS/CSS accordingly, without changing the HTML markup.
How do I apply JavaScript slide up/down functionality to accordion panels when opened?
function initAccordion() { let handlePanelClick = (e) => { showPanel(e.currentTarget.parentNode); }; function showPanel(panel) { let isWide = isWideScreen(); let isActive = panel.classList.contains("active"); if (!isActive) { panel.classList.add("active"); panel.style.height = "auto"; var height = panel.clientHeight + "px"; panel.style.height = "0px"; setTimeout(() => { panel.style.height = height }, 0); } else { panel.style.height = "auto"; panel.addEventListener( "transitionend", () => { panel.classList.remove("active"); }, { once: true } ); } if (isWide) { if (!isActive) { panel.classList.add("active"); } else { panel.classList.remove("active"); } } else { // handle mobiles let panels = document.querySelectorAll(".panel"); panels.forEach((p) => p.classList.remove("active")); if (!isActive) { // open the panel panel.classList.add("active"); } } } let allPanelElements = document.querySelectorAll(".panel"); allPanelElements.forEach((allPanels) => allPanels .querySelector(".acc-trigger") .addEventListener("click", handlePanelClick) ); } let isWideScreen = () => { return window.matchMedia("(min-width: 992px)").matches; }; initAccordion(document.getElementsByClassName("accordion"));
.wrapper { margin: 50px auto 0; display: flex; flex-direction: column; } .wrapper a { text-decoration: none; } .wrapper button { border-style: none; background: white; } .wrapper button::-moz-focus-inner { border: 0; } .wrapper .accordion:first-child { margin-right: 1rem; } .wrapper .accordion .panel { min-height: unset; margin: 0; border-bottom: 1px solid #000A70; transition: height 1s ease; overflow: hidden; } .wrapper .accordion .panel .acc-trigger { display: block; font-weight: 400; margin: 0; padding: 0; position: relative; text-align: left; width: 100%; cursor: pointer; background: 0 0; } .wrapper .accordion .panel .acc-trigger .acc-title { display: flex; align-items: center; outline: 0; padding-bottom: 0.5rem; transition: all 1s ease; } .wrapper .accordion .panel .acc-trigger .acc-title:hover svg.acc-icon path { fill: #005fec; } .wrapper .accordion .panel .acc-trigger .acc-title h4 { font-weight: 800; color: #000A70; font-size: 1.25rem; justify-content: space-between; margin-right: 1rem; outline: 0; transition: all 1s ease; } .wrapper .accordion .panel .acc-trigger .acc-title h4:hover { color: #005fec; } .wrapper .accordion .panel .acc-trigger .acc-title svg.acc-icon { cursor: pointer; pointer-events: none; margin-left: auto; z-index: 3; } .wrapper .accordion .panel .acc-body { height: 0; opacity: 0; overflow: hidden; visibility: hidden; transition: height 1s ease-out, opacity 1s ease-out; } .wrapper .accordion .panel .acc-body ol.custom-list { list-style-type: none; -webkit-margin-before: 0; margin-block-start: 0; -webkit-margin-after: 0; margin-block-end: 0; counter-reset: my-awesome-counter; } .wrapper .accordion .panel .acc-body ol.custom-list li { counter-increment: my-awesome-counter; padding-left: 1.125rem; margin: 2rem 0; font-size: 1rem; line-height: 1.5rem; color: #000A70; } .wrapper .accordion .panel .acc-body ol.custom-list li::before { content: counter(my-awesome-counter) ". "; color: #005fec; } .wrapper .accordion .panel .acc-body ol.custom-list li:first-child { margin-top: 0; } .wrapper .accordion .panel .acc-body ol.custom-list li:last-child { margin-bottom: 2rem; } .wrapper .accordion .panel .acc-body ol.custom-list li a { color: #005fec; } .wrapper .accordion .panel .acc-body ol.custom-list li a:hover { text-decoration: underline; -webkit-text-decoration-color: #005fec; text-decoration-color: #005fec; } .wrapper .accordion .panel .acc-body ul.custom-list { list-style-type: none; -webkit-margin-before: 0; margin-block-start: 0; -webkit-margin-after: 0; margin-block-end: 0; } .wrapper .accordion .panel .acc-body ul.custom-list li { background-image: url(/assets/svg/bullet.svg); background-position: 0 0.5rem; background-repeat: no-repeat; padding-left: 1.125rem; margin: 2rem 0; font-size: 1rem; line-height: 1.5rem; } .wrapper .accordion .panel .acc-body ul.custom-list li a { color: #005fec; } .wrapper .accordion .panel .acc-body ul.custom-list li a:hover { text-decoration: underline; -webkit-text-decoration-color: #005fec; text-decoration-color: #005fec; } .wrapper .accordion .panel .acc-body ul.custom-list li:first-child { margin-top: 0; } .wrapper .accordion .panel .acc-body ul.custom-list li:last-child { margin-bottom: 2rem; } .wrapper .accordion .panel + .panel { margin-top: 1rem; } .wrapper .accordion .panel.active .acc-body { opacity: 1; height: auto; padding: 0.5rem 0; visibility: visible; } .wrapper .accordion .panel.active .acc-body p { font-size: 1rem; line-height: 1.5rem; } .wrapper .accordion .panel.active .acc-body p + .embed-youtube { margin: 1rem 0; } .wrapper .accordion .panel.active .acc-body p a { color: #005fec; } .wrapper .accordion .panel.active .acc-body p a:hover { text-decoration: underline; -webkit-text-decoration-color: #005fec; text-decoration-color: #005fec; } .wrapper .accordion .panel.active .acc-icon { transform: rotate(180deg); } .wrapper .accordion .panel.active .acc-trigger .acc-title { border-bottom: 0; } .wrapper .accordion .panel.active p:last-child { padding-bottom: 0.5rem; } .acc-title svg.acc-icon { transition: all 1s ease; min-width: 1rem; } @media (min-width: 768px) { .wrapper { flex-direction: row; } .accordion { flex: 1; } }
<section id="questions"> <div class="container"> <div class="row justify-content-center align-items-start"> <h3>Any Questions?</h3> <div class="wrapper"> <div class="accordion"> <div class="panel"> <button class="acc-trigger"> <span class="acc-title"> <h4>Lorem ipsum dolor sit amet.</h4> <svg class="acc-icon" width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M0 2.82813L2 0.828126L8 6.82813L14 0.828126L16 2.82813L8 10.8281L0 2.82813Z" fill="#000A70" /> </svg> </span> </button> <div class="acc-body"> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> </div> </div> <div class="panel"> <button class="acc-trigger"> <span class="acc-title"> <h4>Lorem ipsum dolor sit amet.</h4> <svg class="acc-icon" width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M0 2.82813L2 0.828126L8 6.82813L14 0.828126L16 2.82813L8 10.8281L0 2.82813Z" fill="#000A70" /> </svg> </span> </button> <div class="acc-body"> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> </div> </div> </div> </div> </div> </div> </section>
Advertisement
Answer
Since you can’t transition/animate between height:0px and auto you might try this approach:
We’re hiding all accordion contents on load in a way that still allows us to calculate their individual heights by js.
.acc-body { position: absolute; height: auto; }
We can now store these height values as a data-attribute added to each accordion body.
When a accordion section gets expanded, this height value is applied as a max-height inline-style.
Example 1: Accordion
// get acc heights let accordionContents = document.querySelectorAll('.acc-body'); getAccordionHeights(accordionContents); function getAccordionHeights(accordionContents) { accordionContents.forEach(function(acc, i) { // reset heights on resize if (acc.classList.contains('acc-body-init')) { acc.removeAttribute('style'); acc.classList.remove('acc-body-init'); console.log('acc heights recalculated') } let bb = acc.getBoundingClientRect(); let h = bb.height; acc.setAttribute('data-height', (h * 1.5) + 'px'); acc.classList.add('acc-body-init'); }) } // add click event handlers let accTriggers = document.querySelectorAll('.acc-trigger'); accTriggers.forEach(function(btn, i) { btn.addEventListener('click', function(e) { let panel = e.currentTarget.closest('.panel'); let btn = panel.querySelector('.acc-trigger'); let acc = panel.querySelector('.acc-body'); let accHeight = acc.getAttribute('data-height'); let opened = document.querySelectorAll('.active'); // toggle this btn state panel.classList.toggle('active'); // apply max-height from data-attribute if (panel.classList.contains('active')) { acc.style.maxHeight = accHeight; btn.setAttribute('aria-expanded', 'true'); acc.setAttribute('aria-hidden', 'false'); } else { acc.style.maxHeight = '0px'; btn.setAttribute('aria-expanded', 'false'); acc.setAttribute('aria-hidden', 'true'); } if (!isWideScreen()) { closeOpened(opened) } }) }); // add close all previously opened function closeOpened(opened) { opened.forEach(function(panel, i) { let acc = panel.querySelector('.acc-body'); let btn = panel.querySelector('.acc-trigger'); panel.classList.remove('active'); acc.style.maxHeight = '0px'; btn.setAttribute('aria-expanded', 'false'); acc.setAttribute('aria-hidden', 'true'); }); } let isWideScreen = () => { return window.matchMedia("(min-width: 992px)").matches; }; // recalculate heights on window resize window.onresize = function(e) { let opened = document.querySelectorAll('.active'); getAccordionHeights(accordionContents); closeOpened(opened) }
.acc-icon { display: inline-block; height: 1em; } .wrapper { margin: 50px auto 0; display: flex; flex-direction: column; } .wrapper a { text-decoration: none; } .wrapper button { border-style: none; background: white; } .wrapper button::-moz-focus-inner { border: 0; } .wrapper .accordion:first-child { margin-right: 1rem; } .panel { margin: 0; border-bottom: 1px solid #000A70; overflow: hidden; position: relative; } .acc-trigger { display: flex; align-items: center; outline: 0; padding-bottom: 0.5rem; font-weight: 400; margin: 0; padding: 0; position: relative; text-align: left; width: 100%; cursor: pointer; background: 0 0; padding: 1em 0; } .acc-icon { color: inherit; pointer-events: none; margin-left: auto; z-index: 3; transition: all 1s ease; align-self: flex-start; padding: 0.5em 0; width: 1em; height: 1em; font-size: 1.2em; } .acc-trigger h4 { font-weight: 800; font-size: 1.25rem; outline: 0; padding: 0; margin: 0; margin-right: 1rem; } .acc-trigger:focus, .acc-trigger:hover { color: #005fec; } /*** hidden acc body before initialisation to get heights */ .acc-body { position: absolute; height: auto; visibility: hidden; clip: rect(0px auto 2px 0px); font-size: 1rem; line-height: 1.5rem; padding: 0.5rem 0; } .acc-body-init { position: relative; overflow: hidden; max-height: 0px; clip: unset; transition: all 0.5s ease-out; } .active .acc-body-init { max-height: 500px; padding-bottom: 1em; visibility: visible; } .acc-body p a { color: #005fec; } .acc-body p a:hover { text-decoration: underline; -webkit-text-decoration-color: #005fec; text-decoration-color: #005fec; } .active .acc-icon { transform: rotate(180deg); } .active .acc-trigger { border-bottom-color: transparent; } .panel p:last-child { padding-bottom: 0.5rem; } @media (min-width: 768px) { .wrapper { flex-direction: row; } .accordion { flex: 1; } }
<svg style="display:none" class="acc-icon" width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg"> <symbol id="btn-acc" viewBox="0 0 16 11"> <path d="M0 2.82813L2 0.828126L8 6.82813L14 0.828126L16 2.82813L8 10.8281L0 2.82813Z" fill="currentColor" /> </symbol> </svg> <section id="questions"> <div class="wrapper"> <div class="accordion"> <div class="panel"> <button class="acc-trigger" aria-expanded="false" id="accBtn01"> <h4>Lorem ipsum dolor sit amet.</h4> <svg class="acc-icon" viewBox="0 0 16 11" > <use href="#btn-acc"/> </svg> </button> <div class="acc-body" aria-hidden="true" aria-labelledby="accBtn01"> <p>Lorem ipsum dolor sit amet, <a href="#">consectetur</a> adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> </div> </div> <div class="panel"> <button class="acc-trigger" aria-expanded="false" id="accBtn02"> <h4>Lorem ipsum dolor sit amet. <br />Lorem ipsum dolor sit amet. </h4> <svg class="acc-icon" viewBox="0 0 16 11" > <use href="#btn-acc"/> </svg> </button> <div class="acc-body" aria-hidden="true" aria-labelledby="accBtn02"> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> </div> </div> </div> </div> </section>
You might also need a window resize event handler to recalculate heights, when the window resizing occurs.
I also strongly recommend to add appropriate aria-attributes for better accessibility.
Example 2: height checking on click
// add click event handlers const selectorPanel = '.panel'; const selectorTrigger = '.acc-trigger'; const selectorAccContent = '.acc-body'; let accTriggers = document.querySelectorAll(selectorTrigger); let accordionContents = document.querySelectorAll(selectorAccContent); let panels = document.querySelectorAll(selectorPanel); // add aria attributes autoAriaAttributes(panels); function autoAriaAttributes(panels){ panels.forEach(function (panel, i) { let trigger = panel.querySelector(selectorTrigger); let acc = panel.querySelector(selectorAccContent); trigger.setAttribute('aria-expanded', 'false'); if(!trigger.id){ trigger.id = 'accTrigger-'+i; } trigger.setAttribute('aria-expanded', 'false'); acc.setAttribute('aria-labelledby', trigger.id ); acc.setAttribute('aria-hidden', 'true' ); }); } accTriggers.forEach(function (btn, i) { btn.addEventListener('click', function (e) { let panel = e.currentTarget.closest(selectorPanel); let btn = panel.querySelector(selectorTrigger); let acc = panel.querySelector(selectorAccContent); let opened = document.querySelectorAll('.active'); // toggle this btn state panel.classList.toggle('active'); let accHeight = acc.scrollHeight * 1.5; // apply max-height from data-attribute if (panel.classList.contains('active')) { acc.style.maxHeight = accHeight+'px'; btn.setAttribute('aria-expanded', 'true'); acc.setAttribute('aria-hidden', 'false'); }else{ acc.style.maxHeight = null; btn.setAttribute('aria-expanded', 'false'); acc.setAttribute('aria-hidden', 'true'); } if(!isWideScreen() ){ closeOpened(opened) } }) }); // add close all previously opened function closeOpened(opened) { opened.forEach(function (panel, i) { let acc = panel.querySelector(selectorAccContent); let btn = panel.querySelector(selectorTrigger); panel.classList.remove('active'); acc.style.maxHeight = null; btn.setAttribute('aria-expanded', 'false'); acc.setAttribute('aria-hidden', 'true'); }); } let isWideScreen = () => { return window.matchMedia("(min-width: 992px)").matches; }; // recalculate heights on window resize window.onresize = function(e) { let opened = document.querySelectorAll('.active'); closeOpened(opened) }
.wrapper { margin: 50px auto 0; display: flex; flex-direction: column; } .wrapper a { text-decoration: none; } .wrapper button { border-style: none; background: white; } .wrapper button::-moz-focus-inner { border: 0; } .wrapper .accordion:first-child { margin-right: 1rem; } .acc-trigger { display: flex; align-items: center; outline: 0; padding-bottom: 0.5rem; font-weight: 400; margin: 0; padding: 0; position: relative; text-align: left; width: 100%; cursor: pointer; background: 0 0; padding: 1em 0; } .acc-icon { display: inline-block; color: inherit; pointer-events: none; margin-left: auto; z-index: 3; transition: all 1s ease; align-self: flex-start; padding: 0.5em 0; width: 1em; height: 1em; font-size: 1.2em; } .acc-trigger h4 { font-weight: 800; font-size: 1.25rem; outline: 0; padding: 0; margin: 0; margin-right: 1rem; } .acc-trigger:focus, .acc-trigger:hover { color: #005fec; } .panel { margin: 0; border-bottom: 1px solid #000A70; overflow: hidden; } /** * hidden acc body before initialisation to get heights * visibility is set to hidden * to prevent invisible elements from being focused */ .acc-body { position: relative; overflow: hidden; visibility: hidden; max-height: 0px; transition-property: max-height, visibility; transition-delay: 0s; transition-duration: 0.5s; transition-timing-function: ease-out, ease-out; padding: 0.5rem 0; } .active .acc-body { padding-bottom: 2em; visibility: visible; overflow: visible; } .acc-body p a { color: #005fec; } .acc-body p a:hover { text-decoration: underline; -webkit-text-decoration-color: #005fec; text-decoration-color: #005fec; } .active .acc-icon { transform: rotate(180deg); } .active .acc-trigger { border-bottom-color: transparent; } .panel p:last-child { padding-bottom: 0.5rem; } @media (min-width: 768px) { .wrapper { flex-direction: row; } .accordion { flex: 1; } }
<style> body { font-size: 2em; line-height: 2em; } .resizable { overflow: auto; resize: both; border: 1px solid #ccc; padding: 1em; } #questions { width: 800px; max-width: 90%; margin: 0 auto; } </style> <!-- fallback if js disabled --> <noscript> <style> .acc-body { padding-bottom: 2em; visibility: visible; overflow: visible; max-height:inherit; } </style> </noscript> <svg style="display:none" class="acc-icon" width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg"> <symbol id="btn-acc" viewBox="0 0 16 11"> <path d="M0 2.82813L2 0.828126L8 6.82813L14 0.828126L16 2.82813L8 10.8281L0 2.82813Z" fill="currentColor" /> </symbol> </svg> <section class="resizable" id="questions"> <div class="wrapper"> <div class="accordion"> <div class="panel"> <button class="acc-trigger"> <h4>Lorem ipsum dolor sit amet.</h4> <svg class="acc-icon" viewBox="0 0 16 11"> <use href="#btn-acc" /> </svg> </button> <div class="acc-body" style="color:#777"> <p>Lorem ipsum dolor sit amet, <a href="#">focusable link</a> consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam. <strong>Last</strong></p> </div> </div> <div class="panel"> <button class="acc-trigger"> <h4>Lorem ipsum dolor sit amet. <br />Lorem ipsum dolor sit amet. </h4> <svg class="acc-icon" viewBox="0 0 16 11"> <use href="#btn-acc" /> </svg> </button> <div class="acc-body"> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam.</p> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi enim quas laboriosam itaque harum ab voluptas? Deserunt aliquam pariatur voluptatem odit iure autem eos totam eius consequuntur animi. Omnis, ipsam. <strong>Last</strong></p> </div> </div> </div> </div> </section>
Edits:
To improve accessibility, collapsed accordion have visibility set to hidden to prevent hidden elements from getting focus (try to navigate via Tab key).
Basic ARIA attributes for visibility states can be added automatically.
This snippet will calculate the necessary max-height values on click via element.scrollHeight
Be careful: due to reflows this value might change after exapanding the accordion – therefore you might need some additional height like
let accHeight = acc.scrollHeight * 1.5
;
There is also a “no-js” fallback to ensure all content is displayed, when js is disabled.