22 CSS Dropdown Menu Designs 01 / 22

Slide-Down Fade Dropdown

Classic navigation dropdown that slides down and fades in on hover using max-height and opacity transitions on a CSS-only nav.

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

The code

<div class="dd-01">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
  <nav class="dd-01__nav">
    <div class="dd-01__item">
      <a href="#" class="dd-01__trigger">Products <span class="dd-01__caret">▾</span></a>
      <ul class="dd-01__panel">
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> Design System</a></li>
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> Component Library</a></li>
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> Icon Pack</a></li>
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> Templates</a></li>
      </ul>
    </div>
    <div class="dd-01__item">
      <a href="#" class="dd-01__trigger">Solutions <span class="dd-01__caret">▾</span></a>
      <ul class="dd-01__panel">
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> For Startups</a></li>
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> For Enterprise</a></li>
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> For Agencies</a></li>
      </ul>
    </div>
    <div class="dd-01__item">
      <a href="#" class="dd-01__trigger">Resources <span class="dd-01__caret">▾</span></a>
      <ul class="dd-01__panel">
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> Documentation</a></li>
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> Blog</a></li>
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> Community</a></li>
        <li><a href="#"><span class="dd-01__icon">&#9670;</span> Changelog</a></li>
      </ul>
    </div>
    <a href="#" class="dd-01__cta">Get Started</a>
  </nav>
</div>
.dd-01, .dd-01 *, .dd-01 *::before, .dd-01 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.dd-01 ::selection { background: #6366f1; color: #fff; }

.dd-01 {
  --brand: #6366f1;
  --brand-dark: #4f46e5;
  --surface: #ffffff;
  --border: #e5e7eb;
  --text: #111827;
  --muted: #6b7280;
  --hover-bg: #f5f3ff;
  --shadow: 0 10px 40px rgba(99,102,241,.15);
  font-family: 'Inter', sans-serif;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 340px;
  background: linear-gradient(135deg, #f0f4ff 0%, #faf5ff 100%);
  padding: 40px 20px;
}

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

.dd-01__item {
  position: relative;
}
.dd-01__item::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 100%;
  height: 8px;
  /* 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-01__trigger {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 8px 14px;
  color: var(--text);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  border-radius: 8px;
  transition: background 0.18s, color 0.18s;
  white-space: nowrap;
}
.dd-01__trigger:hover { background: var(--hover-bg); color: var(--brand); }

.dd-01__caret {
  font-size: 12px;
  display: inline-block;
  transition: transform 0.25s ease;
  color: var(--muted);
  line-height: 1;
  margin-top: 1px;
}
.dd-01__item:hover .dd-01__caret { transform: rotate(180deg); color: var(--brand); }

/* ── dropdown panel ── */
.dd-01__panel {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(-6px);
  min-width: 200px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow);
  list-style: none;
  padding: 6px;
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition:
    max-height 0.35s ease,
    opacity 0.22s ease 0.05s,
    transform 0.28s ease;
}
.dd-01__item:hover .dd-01__panel {
  max-height: 320px;
  opacity: 1;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}

.dd-01__panel li a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: 8px;
  text-decoration: none;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 450;
  transition: background 0.15s, color 0.15s;
}
.dd-01__panel li a:hover {
  background: var(--hover-bg);
  color: var(--brand);
}

.dd-01__icon {
  font-size: 7px;
  color: var(--brand);
  opacity: 0.6;
}

.dd-01__cta {
  margin-left: 8px;
  padding: 8px 18px;
  background: var(--brand);
  color: #fff;
  border-radius: 8px;
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  transition: background 0.18s, transform 0.15s;
}
.dd-01__cta:hover { background: var(--brand-dark); transform: translateY(-1px); }

@media (prefers-reduced-motion: reduce) {
  .dd-01__panel, .dd-01__caret, .dd-01__trigger, .dd-01__cta { transition: none; }
}

How this works

The dropdown panel sits in the normal flow beneath its trigger but starts at max-height: 0; overflow: hidden; opacity: 0, making it invisible with zero height. When the parent .dd-01__item receives :hover, a sibling selector targets the nested .dd-01__panel and transitions max-height to a sufficiently large value and opacity to 1.

Two separate transition durations are applied — max-height 0.35s ease and opacity 0.25s ease 0.05s — so the container grows first and the content fades in slightly after, avoiding the jarring simultaneous pop. A subtle translateY(-6px) → translateY(0) on the panel adds physical depth to the reveal without any JS.

Customize

  • Adjust reveal speed by changing the max-height transition duration from 0.35s to 0.5s for a slower, more deliberate feel.
  • Add a caret indicator by using a ::after pseudo-element with border trick on the trigger, rotating it on hover with transform: rotate(180deg).
  • Change the panel shadow depth by editing box-shadow: 0 8px 32px rgba(0,0,0,.12) — increase the blur radius for more dramatic elevation.
  • To keep the menu open on panel hover too, wrap trigger + panel in a single container and apply :hover to the wrapper instead.

Watch out for

  • max-height transitions have a known speed distortion — if the actual content is 120px but max-height is set to 600px, the animation appears to slow near the end. Set it as close to actual height as practical.
  • On touch devices, :hover fires on first tap and stays active, meaning the menu never closes. Pair with a JS click-outside handler in production.
  • Screen readers do not treat overflow: hidden elements as hidden — add aria-expanded and aria-hidden attributes for full accessibility.

Browser support

ChromeSafariFirefoxEdge
49+ 9+ 44+ 49+

Fully supported everywhere; no prefixes needed for transitions.

Search CodeFronts

Loading…