22 CSS Dropdown Menu Designs 06 / 22

Stagger Children Reveal Dropdown

Each menu item cascades in one-by-one with staggered animation-delay values applied via nth-child, creating a flowing sequential reveal.

Pure CSS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="dd-06">
  <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
  <nav class="dd-06__nav">
    <span class="dd-06__brand">&#9670; Flow</span>
    <div class="dd-06__items">
      <div class="dd-06__item">
        <a href="#" class="dd-06__trigger">
          <span>Products</span>
          <span class="dd-06__arrow">▾</span>
        </a>
        <div class="dd-06__panel">
          <a href="#" class="dd-06__link" style="--i:0"><span class="dd-06__dot" style="--c:#6366f1"></span>Design Tokens</a>
          <a href="#" class="dd-06__link" style="--i:1"><span class="dd-06__dot" style="--c:#ec4899"></span>Component Kit</a>
          <a href="#" class="dd-06__link" style="--i:2"><span class="dd-06__dot" style="--c:#f59e0b"></span>Icon Library</a>
          <a href="#" class="dd-06__link" style="--i:3"><span class="dd-06__dot" style="--c:#10b981"></span>Figma Plugin</a>
          <a href="#" class="dd-06__link" style="--i:4"><span class="dd-06__dot" style="--c:#3b82f6"></span>CLI Tools</a>
        </div>
      </div>
      <div class="dd-06__item">
        <a href="#" class="dd-06__trigger">
          <span>Use Cases</span>
          <span class="dd-06__arrow">▾</span>
        </a>
        <div class="dd-06__panel">
          <a href="#" class="dd-06__link" style="--i:0"><span class="dd-06__dot" style="--c:#8b5cf6"></span>Startups</a>
          <a href="#" class="dd-06__link" style="--i:1"><span class="dd-06__dot" style="--c:#06b6d4"></span>Agencies</a>
          <a href="#" class="dd-06__link" style="--i:2"><span class="dd-06__dot" style="--c:#f97316"></span>Freelancers</a>
          <a href="#" class="dd-06__link" style="--i:3"><span class="dd-06__dot" style="--c:#14b8a6"></span>Enterprise</a>
        </div>
      </div>
      <div class="dd-06__item">
        <a href="#" class="dd-06__trigger">
          <span>Company</span>
          <span class="dd-06__arrow">▾</span>
        </a>
        <div class="dd-06__panel">
          <a href="#" class="dd-06__link" style="--i:0"><span class="dd-06__dot" style="--c:#ef4444"></span>About</a>
          <a href="#" class="dd-06__link" style="--i:1"><span class="dd-06__dot" style="--c:#a855f7"></span>Blog</a>
          <a href="#" class="dd-06__link" style="--i:2"><span class="dd-06__dot" style="--c:#0ea5e9"></span>Careers</a>
        </div>
      </div>
    </div>
  </nav>
</div>
.dd-06, .dd-06 *, .dd-06 *::before, .dd-06 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.dd-06 ::selection { background: #6366f1; color: #fff; }

.dd-06 {
  --surface: #fff;
  --text: #1e293b;
  --border: #f1f5f9;
  --hover: #f8fafc;
  font-family: 'Nunito', sans-serif;
  min-height: 360px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 32px 20px;
  background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 50%, #f0f9ff 100%);
}

.dd-06__nav {
  display: flex;
  align-items: center;
  gap: 4px;
  background: var(--surface);
  border: 1px solid #e2e8f0;
  border-radius: 14px;
  padding: 10px 10px 10px 20px;
  box-shadow: 0 4px 20px rgba(0,0,0,.06);
  position: relative;
  z-index: 100;
}

.dd-06__brand {
  font-size: 17px;
  font-weight: 800;
  color: #6366f1;
  margin-right: 12px;
  letter-spacing: -0.3px;
}

.dd-06__items { display: flex; gap: 2px; }
.dd-06__item { position: relative; }
.dd-06__item::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 100%;
  height: 10px;
  /* hover-bridge: invisible strip below the trigger covering
     the visible gap before the panel. Lives on .__item (not
     the panel, which has overflow:hidden / pointer-events:
     none in its closed state) so the parent :hover stays
     active while the cursor traverses the gap. */
}

.dd-06__trigger {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 10px;
  color: var(--text);
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  transition: background 0.15s, color 0.15s;
}
.dd-06__trigger:hover, .dd-06__item:hover .dd-06__trigger {
  background: #eef2ff;
  color: #6366f1;
}

.dd-06__arrow {
  font-size: 12px;
  transition: transform 0.22s ease;
  color: #94a3b8;
}
.dd-06__item:hover .dd-06__arrow { transform: rotate(180deg); color: #6366f1; }

/* panel */
.dd-06__panel {
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  min-width: 200px;
  background: var(--surface);
  border: 1px solid #e2e8f0;
  border-radius: 14px;
  box-shadow: 0 12px 40px rgba(0,0,0,.10);
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
}
.dd-06__item:hover .dd-06__panel {
  opacity: 1;
  pointer-events: auto;
}

/* stagger children */
@keyframes dd-06-slide-in {
  from { opacity: 0; transform: translateX(-10px); }
  to   { opacity: 1; transform: translateX(0); }
}

.dd-06__link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: 10px;
  text-decoration: none;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 600;
  opacity: 0;
  transition: background 0.15s;
}
.dd-06__item:hover .dd-06__link {
  animation: dd-06-slide-in 0.32s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  animation-delay: calc(var(--i) * 60ms);
}
.dd-06__link:hover { background: var(--hover); }

.dd-06__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--c, #6366f1);
  flex-shrink: 0;
}

@media (prefers-reduced-motion: reduce) {
  .dd-06__link { animation: none; opacity: 1; }
  .dd-06__panel { transition: none; }
}

How this works

The panel wrapper uses a simple opacity 0 → 1 and translateY(-4px) → translateY(0) to appear, but the real effect is on the children. Each .dd-06__link starts with opacity: 0; transform: translateX(-10px) and is assigned an animation that runs when the parent triggers. The key is animation-delay: calc(var(--i) * 60ms) where --i is set via inline style="--i:0" through --i:4 on each child.

Each child plays the dd-06-slide-in keyframe — a simple left-slide + fade — but only when the parent container gets the :hover state applied via the ancestor :hover .dd-06__panel .dd-06__link selector. The animation-fill-mode: both ensures items stay at their end state and don't flash back to hidden after the animation completes.

Customize

  • Speed up the cascade by reducing the stagger increment from 60ms to 40ms in the calc() expression on each child's delay.
  • Change the slide direction from left to bottom by using translateY(10px) in the keyframe instead of translateX(-10px).
  • Add exit animation by targeting :not(:hover) states with a reverse keyframe — though this requires JS class toggling for reliable cross-browser behavior.
  • Use the same technique on icon child elements inside each link for a secondary micro-stagger within each row.

Watch out for

  • CSS animations on hover reset every time the hover state re-triggers — quickly mousing in and out causes items to replay from the start. Use JS class toggling to prevent replays.
  • animation-delay with negative values (e.g. -0.1s) starts the animation mid-playthrough — useful for making early items appear faster.
  • The animation-fill-mode: both is crucial — without it, items flash invisible before their delay elapses and again after the animation finishes.

Browser support

ChromeSafariFirefoxEdge
43+ 9+ 16+ 43+

CSS custom properties as animation delays are supported in all modern browsers.

Search CodeFronts

Loading…