Pure CSS Buttons & forms #hover #ui

Fold-down menu in pure CSS

Hover or focus the button: items swing down from their top edge, one after another.

  • 49 lines of CSS
  • 9 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed
GitHub

How it works

  1. Each item starts folded up: rotateX(-90deg) with transform-origin: top, plus opacity: 0.
  2. On :hover / :focus of the menu they rotate to 0°.
  3. transition-delay: calc(var(--i) * 80ms) opens them one after another.
  4. The non-hover rule uses the reversed delay, so closing runs bottom-up. perspective on the list gives the swing its depth.

Key techniques

  • rotateX(-90deg) → 0 with origin top
  • transition-delay from --i
  • tabindex + :focus for keyboards

Copy-paste code

HTML 9 lines

<div class="menu" tabindex="0">
  <span>Menu ▾</span>
  <ul>
    <li style="--i:0">Profile</li>
    <li style="--i:1">Projects</li>
    <li style="--i:2">Settings</li>
    <li style="--i:3">Sign out</li>
  </ul>
</div>

CSS 49 lines

body {
  place-items: start center;
  padding-top: 40px;
}

.menu {
  position: relative;
  width: 200px;
  cursor: pointer;
  font-family: system-ui;
}

.menu > span {
  display: block;
  padding: 12px 16px;
  border-radius: 10px;
  color: #fff;
  font-weight: 800;
  background: linear-gradient(120deg, #8b6cff, #ff4d9d);
}

.menu ul {
  position: absolute;
  inset: 100% 0 auto;
  margin: 4px 0 0;
  padding: 0;
  list-style: none;
  perspective: 500px;
}

.menu li {
  padding: 10px 16px;
  background: #141830;
  border: 1px solid #2a3054;
  opacity: 0;
  transform-origin: top center;
  transform: rotateX(-90deg);
  pointer-events: none;                            /* folded items must not catch the pointer */
  transition: transform 0.35s cubic-bezier(0.3, 1.4, 0.5, 1), opacity 0.2s;
  transition-delay: calc((3 - var(--i)) * 50ms);   /* closing: bottom-up */
}

.menu:hover li,
.menu:focus li {
  opacity: 1;
  pointer-events: auto;
  transform: rotateX(0deg);
  transition-delay: calc(var(--i) * 80ms);         /* opening: top-down */
}

Sass source 53 lines

.d-dropdown {
  position: relative;
  align-self: start;
  width: 170px;
  margin-top: 26px;
  cursor: pointer;

  > span {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 14px;
    border-radius: 10px;
    background: linear-gradient(120deg, var(--accent), var(--hot));
    color: #fff;
    font-weight: 800;
  }

  ul {
    position: absolute;
    inset: 100% 0 auto;
    margin: 4px 0 0;
    padding: 0;
    list-style: none;
    perspective: 500px;
  }

  // --i is 0…3 from the markup
  li {
    padding: 8px 14px;
    background: var(--surface-solid);
    border: 1px solid var(--border);
    font-size: 14px;
    opacity: 0;
    pointer-events: none; // folded items must not catch the pointer
    transform-origin: top center;
    transform: rotateX(-90deg);
    transition:
      transform 0.35s cubic-bezier(0.3, 1.4, 0.5, 1),
      opacity 0.2s;
    // closing: last item first
    transition-delay: calc((3 - var(--i)) * 50ms);
  }

  &:hover li,
  &:focus li {
    opacity: 1;
    pointer-events: auto;
    transform: rotateX(0deg);
    // opening: first item first
    transition-delay: calc(var(--i) * 80ms);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.