Skip to content

Morphing Hamburger Icon

A three-line menu icon that morphs smoothly into an X.

Freeminimalbrutalist
Category
Components
Added
2026-08-22
Deps
1
Updated
2026-09-01
Preview

Props

28
Requires framer-motion
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
NewPro

A nav that measures itself and collapses what does not fit into a More menu — ghost-row measurement, no breakpoints, no flicker loop.

navigationresponsivemenu

A checkbox whose checkmark draws itself in as a real SVG path animation.

checkboxformsvg

A monthly/yearly billing switch with the price sliding to its new value.

pricingtoggleswitch