Hamburger to Close

The three menu lines fold into an X: the outer lines meet in the middle, then rotate, while the centre line slips away.

  • CSS
  • Buttons
  • Toggle
  • Added

AI prompt

Create a hamburger menu button that morphs into a close icon in pure CSS with a React toggle. Draw three 22px × 2px bars with rounded ends inside a 44px button. Use aria-expanded to drive the state. When expanded, run two steps with the same 360ms total: first move the top and bottom bars to the centre (translateY ±7px), then rotate them to 45deg and -45deg. Do this by transitioning translate and rotate as separate properties with different delays, so they move first and rotate second on open, and the order reverses on close. Fade and scale the middle bar to 0 during the first step. Update the aria-label between "Open menu" and "Close menu". Under prefers-reduced-motion, swap the shapes without transitions.

Source

Demo.tsx
'use client'

import { useState } from 'react'
import s from './demo.module.css'

export default function HamburgerMorph({ labels }: { labels: Record<string, string> }) {
  const [open, setOpen] = useState(false)

  return (
    <button
      type="button"
      className={s.button}
      aria-expanded={open}
      aria-label={open ? labels.close : labels.open}
      onClick={() => setOpen(!open)}
    >
      <span className={s.bar} />
      <span className={s.bar} />
      <span className={s.bar} />
    </button>
  )
}
demo.module.css
.button {
  position: relative;
  width: 56px;
  height: 56px;
  border: 1px solid var(--stage-rule);
  border-radius: 14px;
  background: var(--stage-surface);
  cursor: pointer;
}

.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

.bar {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 22px;
  height: 2px;
  margin: -1px 0 0 -11px;
  border-radius: 2px;
  background: var(--stage-ink);
  /* closing: rotate back first, then spread out */
  transition: translate 180ms var(--ease-out) 180ms, rotate 180ms var(--ease-out), opacity 180ms 180ms, scale 180ms 180ms;
}

.bar:nth-child(1) { translate: 0 -7px; }
.bar:nth-child(3) { translate: 0 7px; }

.button[aria-expanded='true'] .bar {
  /* opening: meet in the middle first, then rotate */
  transition: translate 180ms var(--ease-out), rotate 180ms var(--ease-out) 180ms, opacity 180ms, scale 180ms;
  background: var(--stage-accent);
}

.button[aria-expanded='true'] .bar:nth-child(1) { translate: 0 0; rotate: 45deg; }
.button[aria-expanded='true'] .bar:nth-child(2) { opacity: 0; scale: 0 1; }
.button[aria-expanded='true'] .bar:nth-child(3) { translate: 0 0; rotate: -45deg; }

@media (prefers-reduced-motion: reduce) {
  .bar, .button[aria-expanded='true'] .bar { transition: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.