按鈕與切換

Play/Pause Morph

播放三角形與暫停長條之間平順變形的圖形按鈕,支援受控/非受控雙模式與選填進度環。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add play-pause-morph

還沒有專案,或安裝遇到問題?看快速開始 →

Play/Pause Morph 是用 React、Motion 與 SVG 做的播放/暫停切換按鈕(play pause toggle icon),樣式用 Tailwind CSS。兩個圖示之間是逐點內插 SVG 路徑的變形動畫,按鈕本身是原生 <button>。

Playground

即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。

0.4
編輯中的呼叫預設屬性
<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型別預設值說明
playingboolean—受控播放狀態;提供時由外部主導
defaultPlayingbooleanfalse非受控模式的初始播放狀態
onPlayingChange(playing: boolean) => void—切換時觸發,回傳切換後的狀態
variant"solid" | "soft" | "ghost""solid"視覺變體
size"sm" | "md" | "lg""md"尺寸(32 / 40 / 48px)
progressnumber—選填 0–1 進度;提供時顯示外圈進度環
playLabelstring"播放"暫停中的 aria-label(按下會播放)
pauseLabelstring"暫停"播放中的 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,按下時的縮放回饋也會停用。