Morphing Hamburger Icon
A three-line menu icon that morphs smoothly into an X.
- Category
- Components
- Added
- 2026-08-22
- Deps
- 1
- Updated
- 2026-09-01
Props
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
export function MorphingMenuButton({
size = 28,
menuId = "morphing-menu-panel",
color = "ink",
}: {
size?: number;
menuId?: string;
color?: "ink" | "red";
}) {
const [open, setOpen] = useState(false);
const bar = color === "red" ? "bg-red-500" : "bg-neutral-900";
return (
<button
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
aria-controls={menuId}
onClick={() => setOpen((v) => !v)}
className="relative flex items-center justify-center rounded-full border border-neutral-200 p-3"
style={{ width: size + 24, height: size + 24 }}
>
<span className="relative block" style={{ width: size, height: size * 0.6 }}>
<motion.span
className={"absolute left-0 top-0 h-[2.5px] w-full rounded-full " + bar}
animate={open ? { rotate: 45, y: size * 0.3 } : { rotate: 0, y: 0 }}
transition={{ duration: 0.3, ease: [0.65, 0, 0.35, 1] }}
/>
<motion.span
className={
"absolute left-0 top-1/2 h-[2.5px] w-full -translate-y-1/2 rounded-full " + bar
}
animate={open ? { opacity: 0, x: -8 } : { opacity: 1, x: 0 }}
transition={{ duration: 0.2 }}
/>
<motion.span
className={"absolute bottom-0 left-0 h-[2.5px] w-full rounded-full " + bar}
animate={open ? { rotate: -45, y: -size * 0.3 } : { rotate: 0, y: 0 }}
transition={{ duration: 0.3, ease: [0.65, 0, 0.35, 1] }}
/>
</span>
</button>
);
}
About this component
The morph from three lines to an X works because it's three independently-animated bars, not one shape being reshaped — no SVG path morphing, no icon-library swap-and-crossfade. Each bar is its own absolutely-positioned motion.span: the top and bottom rotate 45/-45 degrees and translate toward the vertical center, the middle just fades out, all interpolated off one boolean. The non-obvious part is the alignment math: the y offset that slides the top and bottom bars toward center is size * 0.3, tied directly to the icon's own dimensions — the bar container is size * 0.6 tall, so half of that is 0.3 * size — change the bar spacing without updating that offset and the X's arms stop meeting cleanly, landing overlapped or gapped. Reach for this over swapping two separate Menu/X icons when the transition needs to read as one continuous shape changing, not two icons taking turns. One easy-to-miss limit: size scales the bars' width and spacing but not their thickness — the 2.5px bar height is hardcoded, so a much larger size renders proportionally thinner bars rather than a cleanly scaled-up icon.
Curator’s note
Each bar animates independently with its own rotate + translate pair rather than trying to fake the X with a single transformed element — that's what lets the top and bottom bars meet cleanly at the center instead of drifting apart or overlapping unevenly.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Priority Plus Nav
componentsA nav that measures itself and collapses what does not fit into a More menu — ghost-row measurement, no breakpoints, no flicker loop.
Animated Checkbox
componentsA checkbox whose checkmark draws itself in as a real SVG path animation.
Pricing Toggle Switch
componentsA monthly/yearly billing switch with the price sliding to its new value.