skip to Main Content

I’m trying to build a nested accordion structure where each accordion, when clicked, updates both its own and its parent’s maxHeight dynamically. However, whenever I open a child accordion, the parent’s height is sometimes computed incorrectly. I expect it to be around 288px, for example, but it ends up at 160px, or part of the content is cut off.

Below is a simplified version of my click handler. When a .aside-accordion button is clicked, I set the child accordion’s maxHeight, then use a while loop to update any parent .accordion-content elements:

document.addEventListener('click', function(event) {
  if (event.target.matches('.aside-accordion')) {
    const button = event.target;
    const accordionMenu = button.nextElementSibling;
    const buttonArrow = button.querySelector('.arrow');

    button.classList.toggle('active');

    if (button.classList.contains('active')) {
      buttonArrow.classList.add('rotate-180');
      accordionMenu.style.maxHeight = accordionMenu.scrollHeight + 'px';
    } else {
      buttonArrow.classList.remove('rotate-180');
      accordionMenu.style.maxHeight = 0;
    }

    let parentContent = accordionMenu.parentElement.closest('.accordion-content');
    while (parentContent) {
      const parentButton = parentContent.previousElementSibling;
      if (parentButton && parentButton.classList.contains('active')) {
        // Here I'm assigning the parent's actual height
        parentContent.style.maxHeight = parentContent.scrollHeight + 'px';
      }
      parentContent = parentContent.parentElement.closest('.accordion-content');
    }
  }
});

Live demo

Expectation: When the child accordion opens, all parent accordions should automatically expand to accommodate the child’s height, so the content is fully visible.

Issue: Sometimes the parent’s maxHeight ends up smaller than expected or partially hides the content, requiring a second click to fix it. I’m using transition: max-height 0.3s ease; overflow: hidden; on all accordions.

I’ve tried using a large hard-coded value (e.g., 999999px) instead of scrollHeight and it “fixes” the issue, but that’s obviously not a clean solution. requestAnimationFrame or small setTimeout calls sometimes help, but still produce inconsistent results. I also tested transitionend, but sometimes the content “jumps” on the initial open.

Question: How can I reliably update the parent maxHeight in a nested accordion so that the first click always shows the correct expanded height (without glitches or having to click again)?

2

Answers


  1. You don’t need to manualy change the height every time.
    Just set height: auto; for the active class in your CSS.

    let toggles = document.querySelectorAll('.nav-list');
    
    toggles.forEach(toggle => {
      let header = toggle.querySelector('.nav-list > .nav-header');
      header.onclick = () => {
        toggle.classList.toggle('open');
      };
    });
    *{
      box-sizing: border-box;
      font-family: 'Helvetica'
    }
    
    body{
      background-color: #010111;
    }
    
    nav{
      color: white;
    }
    
    .nav-header{
      letter-spacing: 0.07rem;
      padding-left: .5em
    }
    
    .nav-container{
      padding-left: 1em;
      height: 0;
      overflow: hidden;
    }
    .nav-list.open > .nav-container{
      height: auto;
    }
    
    
    .nav-list > .nav-header::after{
      content: "05e";
      display: inline-block;
      position: relative;
      bottom: -.6ex;
      right: -.5em;
      font-weight: 600;
      color: #ddd;
    }
    
    .nav-list.open > .nav-header::after{
      transform: rotate(180deg);
      bottom: .25ex;
    }
    
    .nav-item .nav-header{
      font-weight: 100;
    }
    <nav>
      <section class="nav-list">
        <h5 class="nav-header">Cathegory</h5>
        <div class="nav-container">
          <section class="nav-item">
            <h5 class="nav-header">Item</h5>
          </section>
          <section class="nav-item">
            <h5 class="nav-header">Item</h5>
          </section>
          <section class="nav-item">
            <h5 class="nav-header">Item</h5>
          </section>
          <section class="nav-list">
            <h5 class="nav-header">Sub-Cathegory</h5>
            <div class="nav-container">
              <section class="nav-item">
                <h5 class="nav-header">Item</h5>
              </section>
              <section class="nav-item">
                <h5 class="nav-header">Item</h5>
              </section>
            </div>
          </section>
        </div>
      </section>
      <section class="nav-list">
        <h5 class="nav-header">Cathegory</h5>
        <div class="nav-container">
          <section class="nav-item">
            <h5 class="nav-header">Item</h5>
          </section>
          <section class="nav-item">
            <h5 class="nav-header">Item</h5>
          </section>
          <section class="nav-item">
            <h5 class="nav-header">Item</h5>
          </section>
          <section class="nav-list">
            <h5 class="nav-header">Sub-Cathegory</h5>
            <div class="nav-container">
              <section class="nav-item">
                <h5 class="nav-header">Item</h5>
              </section>
              <section class="nav-item">
                <h5 class="nav-header">Item</h5>
              </section>
            </div>
          </section>
        </div>
      </section>
    </nav>
    Login or Signup to reply.
  2. The issue with your implementation likely stems from the fact that setting maxHeight based on scrollHeight can be tricky in nested accordion structures, as the browser may not have fully recalculated the layout before updating the parent elements. Here’s a reliable approach to fix the problem:

    Key Considerations:
    Recalculate Heights After DOM Updates
    When opening a child accordion, the parent’s scrollHeight may not yet include the child’s new height. You need to wait for the browser to finish recalculating the layout before updating the parent’s maxHeight.

    Avoid Conflicts With Transitions
    If transition: max-height is applied while dynamically updating maxHeight, it can cause glitches or jumps. To prevent this, disable the transition temporarily during height recalculations.

    The issue with your implementation likely stems from the fact that setting maxHeight based on scrollHeight can be tricky in nested accordion structures, as the browser may not have fully recalculated the layout before updating the parent elements. Here’s a reliable approach to fix the problem:

    Key Considerations:
    Recalculate Heights After DOM Updates
    When opening a child accordion, the parent’s scrollHeight may not yet include the child’s new height. You need to wait for the browser to finish recalculating the layout before updating the parent’s maxHeight.

    Avoid Conflicts With Transitions
    If transition: max-height is applied while dynamically updating maxHeight, it can cause glitches or jumps. To prevent this, disable the transition temporarily during height recalculations.

    Solution:
    Below is the updated implementation, addressing the issues:

    document.addEventListener('click', function (event) {
      if (event.target.matches('.aside-accordion')) {
        const button = event.target;
        const accordionMenu = button.nextElementSibling;
        const buttonArrow = button.querySelector('.arrow');
    
        button.classList.toggle('active');
    
        if (button.classList.contains('active')) {
          buttonArrow.classList.add('rotate-180');
          accordionMenu.style.maxHeight = accordionMenu.scrollHeight + 'px';
          updateParentHeights(accordionMenu);
        } else {
          buttonArrow.classList.remove('rotate-180');
          accordionMenu.style.maxHeight = 0;
          collapseParentHeights(accordionMenu);
        }
      }
    });
    
    function updateParentHeights(childAccordion) {
      let parentContent = childAccordion.parentElement.closest('.accordion-content');
      while (parentContent) {
        const parentButton = parentContent.previousElementSibling;
        if (parentButton && parentButton.classList.contains('active')) {
          // Temporarily disable transitions for accurate height calculation
          parentContent.style.transition = 'none';
          parentContent.style.maxHeight = 'none'; // Ensure full height calculation
          const updatedHeight = parentContent.scrollHeight + 'px';
          parentContent.style.transition = ''; // Re-enable transition
          parentContent.style.maxHeight = updatedHeight;
        }
        parentContent = parentContent.parentElement.closest('.accordion-content');
      }
    }
    
    function collapseParentHeights(childAccordion) {
      let parentContent = childAccordion.parentElement.closest('.accordion-content');
      while (parentContent) {
        const parentButton = parentContent.previousElementSibling;
        if (parentButton && parentButton.classList.contains('active')) {
          // Adjust parent height after child collapses
          parentContent.style.transition = 'none';
          const updatedHeight = parentContent.scrollHeight + 'px';
          parentContent.style.transition = '';
          parentContent.style.maxHeight = updatedHeight;
        }
        parentContent = parentContent.parentElement.closest('.accordion-content');
      }
    }
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search