1

I have a button to toggle opening and closing of a dropdown menu with CSS transition. Suppose I have this HTML for a dropdown menu and a toggle button:

<button type="button" id="main-nav-collapse-button" aria-label="Click to expand or collapse the main navigation menu" >☰</button>
...
<nav role="navigation" id="main-nav"><ul>
  <li>...</li>
  <li>...</li>
  <li>...</li>
</ul></nav>

Instead of opening/closing the nav dropdown with JS, I have minimal JS to just add/remove a class .expanded to/from the <ul>.

I have transition in the CSS so that the opening/closing is animated:

#main-nav>ul {
  display:none;
  max-height:0;
  overflow:hidden;
  transition: height 0.5s linear;
}
#main-nav>ul.expanded {
  display:block;
  max-height: 100vh;
}

The problem with the above code is that the opening/closing do not transition/animate because I have display CSS property specified in both states. display cannot be transition/animated and it is toggled straight away when the class is added/removed.

In contrast, if I remove the display properties in the CSS, it does animate. The only problem is that the menu is only hidden from users (height=0) but not preventing the menu from being accessed. When users use keyboard-navigation by tapping , the menu items in the menu are still focusable even they are visibly 'hidden'. I haven't found a solution to disable the focus with CSS.

I am hoping there is a way to apply the display property change before/after the CSS transition. I haven't got a pure CSS approach. My current fallback is to apply the change of display property before/after the class-toggle with a delay using JS, but I just feel that this approach is more like a patch to the problem rather than a proper solution.

It will be great if there is a non-JS solution.

Side note: I could have made the class-toggling part non-JS-dependent too, but unfortunately the button and the nav don't share the same parent in DOM. Making the dropdown to appear/hide on hover without JS would be extremely difficult.

0

4 Answers 4

3

In a perfect world the <details> element would be the "goto" solution for a dropdown. Unfortunately the world is far from perfect and Chrome is the only browser that supports the CSS properties needed to animate <details>.

ATM, the example below is the only way (that I'm aware of) to make <details> animated for Firefox and Chrome (My Mac died 🪦 so I don't know about Safari).

  • place a block element "behind"/"under"/"after" the <details> (not inside the <details>). I used a <menu>.

  • wrap <details> and the new block element (eg <menu>) in another block element, I used a <fieldset>.

  • all animation/transition are assigned to the block elements not the <details>.

Example

Stack Snippets is crap review this CodePen instead.

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
}

:root {
  font: 2ch/1.5 "Segoe UI";
}

body {
  width: 100vw;
  min-height: 100vh;
  overflow-y: scroll;
}

main {
  display: flex;
  place-content: center;
  width: 100%;
  height: 100%;
  margin: 1rem auto;
  padding: 0 0.5rem;
}

details {
  max-width: 12rem;
  overflow: hidden;
}

summary {
  width: 12rem;
  padding: 0.5rem;
  white-space: nowrap;
  color: #fff;
  background: #444;
  cursor: pointer;
}

summary:has(.ico) {
  display: block;
  padding-left: 0;

  &::-webkit-details-marker {
    display: none;
  }
}

menu {
  list-style: none;
  margin: 0;
  padding: 0;
  padding-bottom: 0.75rem;
  border: 2px solid #888;
}

i.ico {
  position: relative;
  display: flex;
  align-items: center;
  height: 1lh;
  padding-left: 0.5rem;
  font-style: normal;

  &::before {
    content: "➤";
    display: flex;
    align-items: center;
    margin-right: 0.5rem;
    transition: rotate 0.2s 0.4s ease-out;
  }
}

details[open] .ico::before {
  rotate: 90deg;
  transition: rotate 200ms ease-out;
}

/* Cross-browserish (haven't tested Safari) */
.box {
  position: relative;
  width: fit-content;
  margin: 0;
  padding: 0;
  border: 0;
  transition: height 1.2s ease-out;
}

.ani {
  &+menu {
    position: absolute;
    top: calc(1lh + 1rem);
    left: 0;
    width: 100%;
    max-height: 0;
    padding: 0 0.625rem;
    border: 2px solid transparent;
    overflow: hidden;
    transition: max-height 1.2s ease-out, border 0ms 1.2s linear;
  }

  &[open]+menu {
    max-height: 1000px;
    padding-bottom: 0.75rem;
    border-color: #888;
    transition: max-height 1.2s ease-out, border 0ms linear;
  }
}
<main>

  <fieldset class="box">
    <details class="ani">
      <summary>
        <i class="ico">Cross-browserish</i>
      </summary>
    </details>
    <menu>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
      <li>XXXXXXXX</li>
    </menu>
  </fieldset>

</main>

Sign up to request clarification or add additional context in comments.

Comments

2

A modern way to toggle an element's height with animation would involve using interpolate-size: allow-keywords;

const elToggleMenu = document.querySelector("#main-nav-collapse-button");
const elMenu = document.querySelector("#main-nav");

elToggleMenu.addEventListener("click", () => {
  elMenu.classList.toggle("expanded");
});
#main-nav {
  background: #ddd;
  interpolate-size: allow-keywords;
  transition: height 0.6s;
  overflow: hidden;
  height: 0;

  &.expanded {
    height: auto;
  }
}
<button type="button" id="main-nav-collapse-button" aria-label="Toggle menu">☰</button>
<nav role="navigation" id="main-nav">
  <ul>
    <li>Lorem</li>
    <li>Ipsum</li>
    <li>Dolor</li>
  </ul>
</nav>

Or alternatively using calc-size()

const elToggleMenu = document.querySelector("#main-nav-collapse-button");
const elMenu = document.querySelector("#main-nav");

elToggleMenu.addEventListener("click", () => {
  elMenu.classList.toggle("expanded");
});
#main-nav {
  background: #ddd;
  transition: height 0.7s;
  overflow: hidden;
  height: 0;

  &.expanded {
    height: calc-size(auto, size); 
  }
}
<button type="button" id="main-nav-collapse-button" aria-label="Toggle menu">☰</button>
<nav role="navigation" id="main-nav">
  <ul>
    <li>Lorem</li>
    <li>Ipsum</li>
    <li>Dolor</li>
  </ul>
</nav>

Comments

1

something like this

#main-nav > ul {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease, opacity 0.5s ease;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

#main-nav > ul.expanded {
  max-height: initial;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

1 Comment

Try to add an explanation instead of just giving code.
1

Here is another exemple of what you could do :

  1. Use max-height to animate the opening.

  2. Use visibility: hidden or opacity: 0 to hide the element.

  3. Use pointer-events: none to prevent interaction and tabbing (browsers respect these for keyboard focus).

This means you get the animation without using display: none, while still avoiding keyboard focus issues.

You can use all of these CSS properties in a selector, of place them in a @keyframes, the choice is yours.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.