Siteplane UI

Icon Morphs

Animate a menu trigger between closed and open states.

Markdown
"use client";

import { MenuIconMorph } from "@/siteplane/components/icon-morphs";

export default function Example() {
  return <MenuIconMorph />;
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/icon-morphs

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/collapsible

Usage

import { MenuIconMorph } from "@/siteplane/components/icon-morphs";

<MenuIconMorph />;

Controlled navigation

Control open to show your navigation and point aria-controls at it. Pass the same open to a Collapsible and render the navigation as its panel: the list expands and collapses with the Collapsible’s motion, which is instant with reduced motion. keepMounted keeps the closed panel in the page, hidden, so aria-controls always finds it. The button sets aria-expanded and switches between “Open navigation menu” and “Close navigation menu”. The links are Buttons with variant="text" and profile="navigation-link", the rows of a mobile menu, and aria-current="page" marks the current one.

"use client";

import { Button } from "@/siteplane/components/button";
import {
  Collapsible,
  CollapsiblePanel,
} from "@/siteplane/components/collapsible";
import { MenuIconMorph } from "@/siteplane/components/icon-morphs";
import { useId, useState } from "react";

const links = [
  { current: true, href: "#overview", label: "Overview" },
  { href: "#projects", label: "Projects" },
  { href: "#settings", label: "Settings" },
];

export default function Example() {
  const [open, setOpen] = useState(false);
  const navigationId = useId();
  return (
    <div className="grid w-full max-w-xs">
      <MenuIconMorph
        open={open}
        onOpenChange={setOpen}
        aria-controls={navigationId}
      />
      <Collapsible open={open}>
        <CollapsiblePanel
          id={navigationId}
          keepMounted
          render={<nav aria-label="Main" />}
        >
          <ul className="grid gap-1 pt-2">
            {links.map((link) => (
              <li key={link.href}>
                <Button
                  align="start"
                  aria-current={link.current ? "page" : undefined}
                  profile="navigation-link"
                  render={<a href={link.href} />}
                  variant="text"
                  width="full"
                >
                  {link.label}
                </Button>
              </li>
            ))}
          </ul>
        </CollapsiblePanel>
      </Collapsible>
    </div>
  );
}

Sizes

Each size uses the matching Button icon size, from icon-sm to icon-xl.

"use client";

import { MenuIconMorph } from "@/siteplane/components/icon-morphs";

export default function Example() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {(["sm", "md", "lg", "xl"] as const).map((size) => (
        <MenuIconMorph key={size} size={size} />
      ))}
    </div>
  );
}

API reference

Component properties
PropTypeDefaultDescription
open / defaultOpenbooleanfalseControlled or initial expanded state.
onOpenChange(open: boolean) => voidunsetRequested next state.
onClickMouseEventHandlerunsetRuns first; preventDefault cancels the toggle.
sizesm | md | lg | xlprovider / lgIcon control size.
aria-labelstringOpen navigation menu / Close navigation menuName reflects state unless overridden.
typebutton | submit | resetbuttonNative button type.
disabledbooleanfalsePrevents changing state.

MenuIconMorphProps extends ButtonBaseProps without aria-expanded, children, size, and variant. The recipe owns aria-expanded, the three bars, and the borderless text styling. className styles the button inside the root span. There are no separate morph parts or menu panel exports.

Read the installed source.

Usage notes

Space and Enter toggle the button. Point aria-controls at the navigation you show. Pass aria-label to replace the default open and close label. With reduced motion, from the operating system or a reduced motion preset, the bars change without animation.