---
title: "CSS Dropdown Menu Animations"
description: "Four free CSS menu animations with previews: fade, grow, staggered cascade, and a hamburger morph. Pure CSS with keyboard accessibility support."
url: https://frontendhelpers.com/css-menu-animations
updated: 2026-10-12
---

# CSS menu & dropdown animations

Dropdown fades, springy grows, staggered cascades and a hamburger that morphs into an X — with keyboard support built in.

## The code for all 4 menus & dropdowns, explained

Every effect above, with the markup to use and the complete CSS. Each one is self-contained: paste the CSS once, add the class, done.

### Dropdown Fade

A gentle pure-opacity fade — no movement at all, the calmest option. visibility joins the transition so the hidden menu can't be tabbed into by accident, and :focus-within keeps it keyboard-accessible.

Markup

```html
<div class="fh-menu">
  <button class="fh-menu-btn">Menu ▾</button>
  <div class="fh-menu-list">
    <a href="#">Profile</a>
    <a href="#">Settings</a>
  </div>
</div>
```

CSS

```css
.fh-menu {
  position: relative;
  display: inline-block;
}
.fh-menu-btn {
  padding: 10px 18px;
  border: 1px solid #3a4358;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  color: inherit;
  font-weight: 600;
  cursor: pointer;
}
.fh-menu-list {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 150px;
  background: #101828;
  border: 1px solid #3a4358;
  border-radius: 10px;
  padding: 6px;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s;
}
.fh-menu:hover .fh-menu-list,
.fh-menu:focus-within .fh-menu-list {
  opacity: 1;
  visibility: visible;
}
.fh-menu-list a {
  display: block;
  padding: 8px 10px;
  border-radius: 6px;
  color: inherit;
  text-decoration: none;
  font-size: 0.9em;
}
.fh-menu-list a:hover { background: rgba(6, 182, 212, 0.12); }
```

### Dropdown Grow

The opposite personality to the fade: the menu springs out of the button corner with a visible overshoot. transform-origin: top left is the spatial cue about where it came from.

Markup

```html
<div class="fh-menu-scale">
  <button>Options ▾</button>
  <div class="fh-menu-pop">…links…</div>
</div>
```

CSS

```css
.fh-menu-scale {
  position: relative;
  display: inline-block;
}
.fh-menu-btn2 {
  padding: 10px 18px;
  border: 1px solid #3a4358;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  color: inherit;
  font-weight: 600;
  cursor: pointer;
}
.fh-menu-pop {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 150px;
  background: #101828;
  border: 1px solid #3a4358;
  border-radius: 10px;
  padding: 6px;
  opacity: 0;
  transform: scale(0.55);
  transform-origin: top left;
  visibility: hidden;
  transition: opacity 0.2s ease,
              transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
              visibility 0.3s;
}
.fh-menu-scale:hover .fh-menu-pop,
.fh-menu-scale:focus-within .fh-menu-pop {
  opacity: 1;
  transform: scale(1);
  visibility: visible;
}
.fh-menu-pop a {
  display: block;
  padding: 8px 10px;
  border-radius: 6px;
  color: inherit;
  text-decoration: none;
  font-size: 0.9em;
}
.fh-menu-pop a:hover { background: rgba(139, 92, 246, 0.15); }
```

### Staggered Items

Each item waits 50ms longer than the one above, so the menu cascades open. Delays reset to zero on close so it snaps shut instantly.

Markup

```html
<div class="fh-menu-stag">
  <button>Products ▾</button>
  <div class="fh-stag-list">…links…</div>
</div>
```

CSS

```css
.fh-menu-stag {
  position: relative;
  display: inline-block;
}
.fh-menu-btn3 {
  padding: 10px 18px;
  border: 1px solid #3a4358;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  color: inherit;
  font-weight: 600;
  cursor: pointer;
}
.fh-stag-list {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 160px;
  background: #101828;
  border: 1px solid #3a4358;
  border-radius: 10px;
  padding: 6px;
  visibility: hidden;
  transition: visibility 0.3s;
}
.fh-stag-list a {
  display: block;
  padding: 8px 10px;
  border-radius: 6px;
  color: inherit;
  text-decoration: none;
  font-size: 0.9em;
  opacity: 0;
  translate: 0 -6px;
  transition: opacity 0.25s ease, translate 0.25s ease;
}
.fh-stag-list a:hover { background: rgba(6, 182, 212, 0.12); }
.fh-menu-stag:hover .fh-stag-list,
.fh-menu-stag:focus-within .fh-stag-list { visibility: visible; }
.fh-menu-stag:hover .fh-stag-list a,
.fh-menu-stag:focus-within .fh-stag-list a {
  opacity: 1;
  translate: 0 0;
}
.fh-menu-stag:hover .fh-stag-list a:nth-child(2) { transition-delay: 0.05s; }
.fh-menu-stag:hover .fh-stag-list a:nth-child(3) { transition-delay: 0.1s; }
```

### Hamburger Morph

The three bars morph into an X using :has() on a visually hidden (but still focusable) checkbox — pure CSS state. For a real navigation menu, use a button with aria-expanded and toggle a class instead, so screen readers announce whether the menu is open.

Markup

```html
<label class="fh-burger" aria-label="Toggle menu">
  <input type="checkbox" />
  <span></span><span></span><span></span>
</label>
```

CSS

```css
.fh-burger {
  width: 46px;
  height: 46px;
  display: grid;
  place-content: center;
  gap: 5px;
  border: 1px solid #3a4358;
  border-radius: 10px;
  cursor: pointer;
  position: relative;
}
/* The checkbox covers the button invisibly, so it stays
   clickable AND reachable with Tab (display: none is neither) */
.fh-burger input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.fh-burger:has(input:focus-visible) {
  outline: 2px solid #22d3ee;
  outline-offset: 2px;
}
.fh-burger span {
  width: 20px;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s ease;
}
.fh-burger:has(input:checked) span:nth-of-type(1) {
  transform: translateY(7px) rotate(45deg);
}
.fh-burger:has(input:checked) span:nth-of-type(2) { opacity: 0; }
.fh-burger:has(input:checked) span:nth-of-type(3) {
  transform: translateY(-7px) rotate(-45deg);
}
```

## The visibility trick that makes dropdowns safe

The most common dropdown bug is hiding a menu with `opacity: 0` alone. An element at zero opacity still occupies space, still receives clicks, and — critically — is still in the keyboard tab order and still announced by screen readers. A "closed" menu built that way silently traps keyboard users in invisible links.

The fix is to include `visibility: hidden` in the transition alongside opacity. Because visibility is discrete rather than continuous, it flips at the end of the transition on close and immediately on open — so the menu fades smoothly but genuinely disappears when closed. Every dropdown here uses that pattern.

## Making hover menus keyboard-accessible

A dropdown that only opens on `:hover` is unreachable by keyboard. Adding `:focus-within` to the same rule fixes it in one line: the menu opens when any descendant receives focus, so tabbing into the trigger reveals it exactly as hovering does.

For production navigation, a click-triggered menu driven by JavaScript is generally better than hover — it works identically on touch, allows explicit Escape-to-close, and lets you manage `aria-expanded` honestly. Use the hover pattern for simple cases and progressive enhancement.

## Transform origin communicates where things come from

The difference between the fade and the grow effect is spatial information. A menu that scales up from `transform-origin: top left` visibly emerges from its button; a menu that simply fades in appears from nowhere. The former helps users connect the panel with the control that opened it — worth the extra line for menus that open in unexpected places.

## Staggering, and knowing when to stop

The staggered effect adds an incremental `transition-delay` per item, cascading them into view. It looks considered on a menu of three to five items and sluggish beyond that — the last item in a ten-item menu arrives long after the user has started reading. Cap the stagger, and set delays back to zero on close so the menu snaps shut instantly.

## Frequently asked questions

### Why is my hidden dropdown still focusable?

Because opacity: 0 alone does not remove an element from the tab order or from screen readers. Add visibility: hidden to the transition so the closed menu genuinely disappears while still fading smoothly.

### How do I make a hover dropdown keyboard-accessible?

Add :focus-within alongside :hover in the same rule, so the menu opens when any descendant receives focus. For production navigation, a click-triggered menu with Escape support and aria-expanded is more robust.

### Should dropdown menus open on hover or click?

Click is generally better: it works identically on touch devices, supports explicit Escape-to-close, and lets you manage aria-expanded accurately. Hover suits simple cases and progressive enhancement.

## Related categories

- [Hover Effects](https://frontendhelpers.com/css-hover-effects): Underline slides, glows and button lifts
 
- [Accordions](https://frontendhelpers.com/css-accordion-animations): Smooth expand, chevron rotate and icon morphs
 
- [UI Feedback](https://frontendhelpers.com/css-ui-feedback-animations): Tooltips, toasts, modals and tab indicators
