汉堡菜单变关闭
三条菜单线折成一个 X:上下两条先在中间汇合再旋转,中间那条悄悄消失。
用纯 CSS 配合 React 状态切换,制作从汉堡菜单变成关闭图标的按钮。在 44px 的按钮内画三条 22px × 2px 的圆头横条。用 aria-expanded 驱动状态。展开时分两步完成,总时长 360ms:先把上下两条移到中间(translateY ±7px),再分别旋转到 45deg 和 -45deg。做法是把 translate 和 rotate 作为两个独立属性过渡,并设置不同的延迟,这样打开时先移动后旋转,关闭时顺序反过来。中间那条在第一步中淡出并缩放到 0。aria-label 在“打开菜单”和“关闭菜单”之间切换。在 prefers-reduced-motion 下直接切换形状,不做过渡。
源代码
'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>
)
}
.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; }
}