Search code examples
htmlcss

Animate a details tag when closing


I am working on a website for my history project and I came across a problem. I tried to animate a details tag but nothing seems to be working. Here is the code I used for the opening animation:

@keyframes open {
  0% {
    opacity: 0;
    transform: translateY(-1vw);
  }
  100% {
    opacity: 1;
    margin-left: 0px
  }
}

details[open] summary~* {
  animation: open .5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
<details>
  <summary>Cfare do te mesosh gjate ketij materiali:</summary>
  <ul>
    <li>
      <p>Shqiperia gjate luftes se pare boterore</p>
    </li>
    <li>
      <p>Shqiperia mes lufterave boterore</p>
    </li>
    <li>
      <p>Shqiperia gjate luftes se dyte boterore</p>
    </li>
  </ul>
</details>

It is basically a simple fade in animation and I would love to do a fade out one as well!


Solution

  • Updated answer:

    The older version was a bit hacky. This is a better approach.

    When you open the details element, its content - by definition - becomes visible. This is why every animation you apply is visible. However, when you close the details element, its content - by definition - becomes invisible, immediately. So even though every animation you try to apply will actually occur, it will simply not be visible.

    So we need to tell the browser to suspend hiding the elements, apply the animation, and only when it's over, to hide the element. Let's use some JS for that.

    We need to capture when the user toggles the details element. Supposedly, the relevant event would be toggle. However, this event is fired only after the browser hides everything. So we will actually go with the click event, which fires before the hiding happens.

    const details = document.querySelector("details");
    details.addEventListener("click", (e) => {
      if (details.hasAttribute("open")) { // since it's not closed yet, it's open!
        e.preventDefault(); // stop the default behavior, meaning - the hiding
        details.classList.add("closing"); // add a class which applies the animation in CSS
      }
    });
    
    // when the "close" animation is over
    details.addEventListener("animationend", (e) => {
      if (e.animationName === "close") {
        details.removeAttribute("open"); // close the element
        details.classList.remove("closing"); // remove the animation
      }
    });
    @keyframes open {
      0% { opacity: 0 }
      100% { opacity: 1 }
    }
    
    /* closing animation */
    @keyframes close {
      0% { opacity: 1 }
      100% { opacity: 0 }
    }
    
    details[open] summary~* {
      animation: open .5s
    }
    
    /* closing class */
    details.closing summary~* {
      animation: close .5s
    }
    <details>
      <summary>Summary</summary>
      <p>
        Details
      </p>
    </details>


    Old answer:

    const details = document.querySelector("details");
    details.addEventListener("click", (e) => {
      if (details.hasAttribute("open")) { // since it's not closed yet, it's open!
        e.preventDefault(); // stop the default behavior, meaning - the hiding
        details.classList.add("closing"); // add a class that applies the animation in CSS
        setTimeout(() => { // only after the animation finishes, continue
          details.removeAttribute("open"); // close the element
          details.classList.remove("closing");
        }, 400);
      }
    });
    @keyframes open {
      0% { opacity: 0 }
      100% { opacity: 1 }
    }
    
    /* closing animation */
    @keyframes close {
      0% { opacity: 1 }
      100% { opacity: 0 }
    }
    
    details[open] summary~* {
      animation: open .5s
    }
    
    /* closing class */
    details.closing summary~* {
      animation: close .5s
    }
    <details>
      <summary>Summary</summary>
      <p>
        Details
      </p>
    </details>

    Note that time durations are not guaranteed to be accurate, not in CSS and not in JS. This is why I have set the JS timeout to be just a little bit lower than the CSS animation duration (400ms vs 500ms), so we are sure the animation ends just a bit after we're hiding the elements, so we don't get any flickering.