# Icon Morphs

Animate a menu trigger between closed and open states.

Menu button

[Complete TSX file](/examples/icon-morphs/default.tsx)

```tsx
"use client";

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

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

## Installation

Complete [one-time setup](/docs/installation/) first.

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

With npm: `npx shadcn@4.18.0 add @siteplane/icon-morphs`. [Registry JSON](/registry/icon-morphs.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/collapsible
```

## Usage

```tsx
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](/docs/components/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.

Menu button controlling navigation

[Complete TSX file](/examples/icon-morphs/controlled.tsx)

```tsx
"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`.

Menu button sizes

[Complete TSX file](/examples/icon-morphs/sizes.tsx)

```tsx
"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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| open / defaultOpen | boolean | false | Controlled or initial expanded state. |
| onOpenChange | (open: boolean) => void | unset | Requested next state. |
| onClick | MouseEventHandler | unset | Runs first; preventDefault cancels the toggle. |
| size | sm \| md \| lg \| xl | provider / lg | Icon control size. |
| aria-label | string | Open navigation menu / Close navigation menu | Name reflects state unless overridden. |
| type | button \| submit \| reset | button | Native button type. |
| disabled | boolean | false | Prevents 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](/source/icon-morphs).

## 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](/docs/customization/motion/), the bars change without animation.
