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.jsonnpm package
For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.
pnpm add suluu motionUsage
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.
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()}
/>
)
}// 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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| aria-label | string | Required | Stable accessible name for both visual states. |
| compactContent | ReactNode | Required | Icon-sized content rendered in the circular state. |
| expandedContent | ReactNode | Required | Content rendered in the expanded pill. |
| expanded | boolean | false | Holds the pill open in addition to hover and focus previews. |
| motionIntensity | "subtle" | "default" | "expressive" | "default" | Spring, rotation, and press character. |
| disabled | boolean | false | Disables activation and previews. |
| className | string | — | Class 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.