Skip to content
Suluu

MorphButton

A compact action that makes room for its meaning exactly when it is needed. Its icon folds inward as the surface springs into a labeled pill, with the same transition available to application state.

Preview

Hover or focus the button

Installation

Registry — recommended

Copies the component into your configured shadcn UI directory and installs Motion.

npx shadcn@latest add https://suluu.site/r/morph-button.json

npm package

For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.

pnpm add suluu motion

Usage

The morphing action is a native button. Hover and keyboard focus preview the label, while the optional controlled state can keep it open for loading, success, or other application feedback.

Registry
import { MorphButton } from "@/components/ui/morph-button"

export function CreateAction() {
  return (
    <MorphButton
      aria-label="Create new"
      compactContent={<PlusIcon />}
      expandedContent={
        <>
          <PlusIcon />
          <span>Create new</span>
        </>
      }
      onClick={() => createItem()}
    />
  )
}
npm
// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";

// component.tsx
import { MorphButton } from "suluu/morph-button"

export function CreateAction() {
  return (
    <MorphButton
      aria-label="Create new"
      compactContent={<PlusIcon />}
      expandedContent={<><PlusIcon /><span>Create new</span></>}
      onClick={() => createItem()}
    />
  )
}

Holding the state

The expanded prop is additive: true keeps the pill open, and false still allows hover and focus previews.

Application state
const [creating, setCreating] = useState(false)

<MorphButton
  aria-label="Create project"
  compactContent={<PlusIcon />}
  expanded={creating}
  expandedContent={
    creating
      ? <span>Creating…</span>
      : <><PlusIcon /><span>Create project</span></>
  }
  onClick={async () => {
    setCreating(true)
    await createProject()
    setCreating(false)
  }}
/>

In context

A compact create action can stay out of the way until its label is useful.

Projects

12 active · updated just now

Props

MorphButton also accepts safe native button attributes, Motion inline styles, and forwards its ref to the button element.

PropTypeDefaultDescription
aria-labelstringRequiredStable accessible name for both visual states.
compactContentReactNodeRequiredIcon-sized content rendered in the circular state.
expandedContentReactNodeRequiredContent rendered in the expanded pill.
expandedbooleanfalseHolds the pill open in addition to hover and focus previews.
motionIntensity"subtle" | "default" | "expressive""default"Spring, rotation, and press character.
disabledbooleanfalseDisables activation and previews.
classNamestringClass name applied to the button.
type"button" | "submit" | "reset""button"Native type.

Theming

Override these variables in your light and dark theme scopes. The registry installs the defaults automatically.

--suluu-morph-backgroundCompact button surface.
--suluu-morph-foregroundCompact icon color.
--suluu-morph-borderCompact button edge.
--suluu-morph-shadowCompact button depth.
--suluu-morph-accentExpanded pill surface.
--suluu-morph-accent-foregroundExpanded pill content.
--suluu-morph-accent-borderExpanded pill edge.
--suluu-morph-accent-shadowExpanded pill depth.
--suluu-morph-ringKeyboard focus ring.
--suluu-morph-offsetSurface behind the focus ring.

The button exposes data-expanded, allowing state-specific styling without changing its behavior.

Accessibility

A required aria-label keeps the accessible name stable while the visual slots crossfade. Keyboard focus previews the expanded label only when focus-visible applies, so pointer clicks do not leave the button unexpectedly open.

Hover previews only run for fine, hover-capable pointers. Touch taps activate immediately in the compact state. Reduced motion preserves the content change while removing layout springs, rotation, blur, and press transforms.

↑↓ to navigate↵ to openesc to close