Play/Pause Morph
A graphical button that morphs smoothly between a play triangle and pause bars, with controlled and uncontrolled modes and an optional progress ring.
npx shadcn@latest add https://webberui.com/r/play-pause-morph.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<PlayPauseMorph />
Installation
npx shadcn@latest add https://webberui.com/r/play-pause-morph.jsonOr, once registries are configured in components.json, install it as @webberui/play-pause-morph.
Usage
import { PlayPauseMorph } from "@/components/ui/play-pause-morph";
<PlayPauseMorph defaultPlaying={false} onPlayingChange={(p) => console.log(p)} />Controlled mode and the progress ring
const [playing, setPlaying] = React.useState(false);
<PlayPauseMorph
size="lg"
playing={playing}
onPlayingChange={setPlaying}
progress={0.42}
/>Passing playing enters controlled mode: the built-in state is disabled and the toggle is entirely driven by your state. Passing progress (0–1) adds a progress ring around the outside of the button.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
playing | boolean | — | Controlled playing state; when provided, it is driven from outside |
defaultPlaying | boolean | false | Initial playing state in uncontrolled mode |
onPlayingChange | (playing: boolean) => void | — | Fires on toggle, returning the state after the toggle |
variant | "solid" | "soft" | "ghost" | "solid" | Visual variant |
size | "sm" | "md" | "lg" | "md" | Size (32 / 40 / 48px) |
progress | number | — | Optional progress from 0–1; when provided, the outer progress ring is shown |
playLabel | string | "播放" | aria-label while paused (pressing it will play) |
pauseLabel | string | "暫停" | aria-label while playing (pressing it will pause) |
All other native <button> attributes (disabled, onClick, className, and so on) are forwarded. Calling preventDefault() inside onClick intercepts that toggle (handy for doing an asynchronous authorization check first).
How it works
- The play triangle is cut down its midline into two quadrilaterals, sharing the same four-point path structure as the two pause bars, which makes it possible to interpolate the SVG
dattribute point by point so the shape genuinely "morphs" rather than cross-fading. - The morph uses a spring transition, so it settles with a natural spring back.
- The progress ring normalizes
pathLengthto 1 and expresses the played ratio throughstrokeDashoffset, tweening smoothly as the value changes.
Accessibility
- Built on a native
<button>, so bothEnterandSpacetrigger it and focus has a clearfocus-visibleoutline. - The
aria-labelfollows the state: "暫停" while playing and "播放" while paused, describing precisely what pressing it will do. - The graphics are hidden from assistive technology with
aria-hidden, so no meaningless vector content is announced. - When the user has "reduce motion" enabled at the system level, the morph and the scale feedback snap between states, with no effect on functionality.
Theme Toggle
A light/dark theme toggle button: View Transitions expand a circle out from the click point, and the Sun/Moon icons rotate through the transition.
Hold to Confirm
A press-and-hold fill confirmation button: holding fills the progress from left to right with a slight tremor near completion, and only a full fill triggers it — preventing accidental taps on dangerous actions.