Play/Pause Morph
播放三角形與暫停長條之間平順變形的圖形按鈕,支援受控/非受控雙模式與選填進度環。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add play-pause-morphPlay/Pause Morph 是用 React、Motion 與 SVG 做的播放/暫停切換按鈕(play pause toggle icon),樣式用 Tailwind CSS。兩個圖示之間是逐點內插 SVG 路徑的變形動畫,按鈕本身是原生 <button>。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<PlayPauseMorph/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- 自訂音樂、Podcast 播放器的主控制鈕
- 影片卡片或封面上的播放疊層按鈕
- 輪播、自動播放投影片的暫停控制
- 語音訊息、有聲書章節列表中每一列的播放鈕
安裝
npx webberui@latest add https://webberui.com/r/play-pause-morph.json或在 components.json 設定 registries 後,改用 @webberui/play-pause-morph 安裝。
使用
import { PlayPauseMorph } from "@/components/ui/play-pause-morph";
<PlayPauseMorph defaultPlaying={false} onPlayingChange={(p) => console.log(p)} />受控模式與進度環
const [playing, setPlaying] = React.useState(false);
<PlayPauseMorph
size="lg"
playing={playing}
onPlayingChange={setPlaying}
progress={0.42}
/>傳入 playing 即進入受控模式,內建狀態停用,開關完全由你的 state 主導。傳入 progress(0–1)時,按鈕外圈會多出一圈進度環。
範例
接上 <audio> 與進度環
用 <audio> 的 play/pause 事件同步受控狀態,timeupdate 換算成 0–1 的 progress。
"use client";
import * as React from "react";
import { PlayPauseMorph } from "@/components/ui/play-pause-morph";
export function EpisodeControl({ src }: { src: string }) {
const audioRef = React.useRef<HTMLAudioElement>(null);
const [playing, setPlaying] = React.useState(false);
const [progress, setProgress] = React.useState(0);
return (
<div className="flex items-center gap-3">
<audio
ref={audioRef}
src={src}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onTimeUpdate={(e) => {
const el = e.currentTarget;
setProgress(el.duration ? el.currentTime / el.duration : 0);
}}
/>
<PlayPauseMorph
size="lg"
playing={playing}
progress={progress}
onPlayingChange={(next) => {
if (next) void audioRef.current?.play();
else audioRef.current?.pause();
}}
/>
</div>
);
}這裡 playing 只跟著 <audio> 的實際狀態更新,瀏覽器擋下自動播放時按鈕也不會誤顯示成播放中。
先檢查登入再播放
在 onClick 呼叫 preventDefault() 可以攔下這次切換,onPlayingChange 不會觸發。
<PlayPauseMorph
onClick={(e) => {
if (!isSignedIn) {
e.preventDefault();
openSignInDialog();
}
}}
onPlayingChange={(next) => (next ? player.play() : player.pause())}
playLabel="播放試聽"
pauseLabel="暫停試聽"
/>自訂顏色
className 會用 cn 與變體樣式合併,可以覆寫背景色;按鈕帶有 data-state="playing" 或 "paused",可用 Tailwind 的 data 變體依狀態換色。
<PlayPauseMorph
variant="solid"
className="bg-emerald-600 text-white hover:bg-emerald-500 data-[state=playing]:bg-rose-600"
/>Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
playing | boolean | — | 受控播放狀態;提供時由外部主導 |
defaultPlaying | boolean | false | 非受控模式的初始播放狀態 |
onPlayingChange | (playing: boolean) => void | — | 切換時觸發,回傳切換後的狀態 |
variant | "solid" | "soft" | "ghost" | "solid" | 視覺變體 |
size | "sm" | "md" | "lg" | "md" | 尺寸(32 / 40 / 48px) |
progress | number | — | 選填 0–1 進度;提供時顯示外圈進度環 |
playLabel | string | "播放" | 暫停中的 aria-label(按下會播放) |
pauseLabel | string | "暫停" | 播放中的 aria-label(按下會暫停) |
其餘 <button> 原生屬性(disabled、onClick、className…)皆可透傳。在 onClick 中呼叫 preventDefault() 可攔下該次切換(適合先做非同步授權)。
細節
- 播放三角形沿中線切成左右兩塊四邊形,與暫停的兩根長條採用相同的四點路徑結構,因此可逐點內插 SVG 的
d屬性,讓圖形真正「變形」而非淡入淡出。 - morph 採用彈簧過場,收放帶有自然的回彈手感。
- 進度環以
pathLength正規化為 1,用strokeDashoffset表示已播放比例,數值變動時平順補間。
可及性
- 以原生
<button>實作,Enter/Space皆可觸發,焦點有清晰的focus-visible外框。 aria-label隨狀態切換:播放中為「暫停」、暫停中為「播放」,明確描述按下後的動作。- 圖形以
aria-hidden對輔助科技隱藏,避免朗讀無意義的向量內容。 - 使用者系統開啟「減少動態效果」時,morph 與縮放回饋直接定格切換,不影響功能。
常見問題
可以在 Next.js App Router 使用嗎?
可以。檔案開頭已經有 "use client",在 Server Component 頁面裡照一般方式 import 即可。
動畫是淡入淡出嗎?
不是。播放三角形被切成兩塊四邊形,與暫停的兩根長條一樣都是四點路徑,Motion 逐點內插 d 屬性並以彈簧過場,所以圖形是連續變形的。
尺寸可以自訂嗎?
size 提供 sm、md、lg(32/40/48px)。其他尺寸可以在 className 傳入 size-14 之類的 class 覆寫,圖示用 viewBox 繪製,會跟著縮放。
支援 disabled 和其他按鈕屬性嗎?
支援。除了少數與 Motion 衝突的拖曳、動畫事件外,原生 <button> 屬性都會透傳;type 預設為 "button",放在表單裡不會意外送出。
有考慮減少動態效果嗎?
有。偵測到「減少動態效果」時,變形與進度環過場的時長設為 0,按下時的縮放回饋也會停用。