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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.