按鈕與切換

Draggable Snap Button

可拖曳並回彈吸附的按鈕:拖到哪、放開後就以 spring 吸附到最近的落點,支援多段落點、方向鍵切換與純回彈模式。

把這個效果裝進你的專案

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

npx webberui@latest add draggable-snap-button

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

Draggable Snap Button 是用 React、Motion 與 Tailwind CSS 做的可拖曳按鈕(drag and snap)元件:放開後依 spring 吸附到最近的落點,可以當成多段切換開關、可移動的浮動按鈕,或拖了會彈回原位的互動按鈕。

Playground

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

0.16
編輯中的呼叫預設屬性
<DraggableSnapButton/>

完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。

適用情境

  • 省電/均衡/效能這類三段模式切換
  • 使用者可以拖到畫面角落停靠的浮動動作按鈕(FAB)
  • 播放器或編輯器中左右分段的工具位置切換
  • 產品首頁裡讓訪客隨手拖拉、放開會彈回的互動元素
  • 不適合:用來確認付款、刪除等不可逆操作,請改用 Slide to Unlock

安裝

npx webberui@latest add https://webberui.com/r/draggable-snap-button.json

或在 components.json 設定 registries 後,改用 @webberui/draggable-snap-button 安裝。

使用

import {
  DraggableSnapButton,
  type SnapPoint,
} from "@/components/ui/draggable-snap-button";

const modes: SnapPoint[] = [
  { x: -100, label: "省電" },
  { x: 0, label: "均衡" },
  { x: 100, label: "效能" },
];

<DraggableSnapButton
  axis="x"
  snapPoints={modes}
  defaultIndex={1}
  onSnapChange={(index, point) => console.log(index, point.label)}
>
  拖曳
</DraggableSnapButton>

不傳 snapPoints 時只有原點一個落點,就是純回彈按鈕——拖到哪、放開都彈回原位:

<DraggableSnapButton dragConstraints={boxRef} dragElastic={0.25}>
  拋看看
</DraggableSnapButton>

範例

受控模式:和其他介面同步

提供 index 後位置由外部主導,在 onSnapChange 裡更新 state;外部改變 index 時按鈕也會吸附過去。

"use client";

import * as React from "react";
import { DraggableSnapButton, type SnapPoint } from "@/components/ui/draggable-snap-button";

const sizes: SnapPoint[] = [
  { x: -80, label: "小" },
  { x: 0, label: "中" },
  { x: 80, label: "大" },
];

export function FontSizeSwitch() {
  const [index, setIndex] = React.useState(1);

  return (
    <div className="flex flex-col items-center gap-4">
      <DraggableSnapButton
        axis="x"
        snapPoints={sizes}
        index={index}
        onSnapChange={(i) => setIndex(i)}
        aria-label="字級"
      >
        {sizes[index].label}
      </DraggableSnapButton>
      <button onClick={() => setIndex(1)}>恢復預設</button>
    </div>
  );
}

停靠到四個角落的浮動按鈕

axis 維持預設 both,落點同時指定 x 與 y,放開後吸附到最近的角落。

const corners: SnapPoint[] = [
  { x: 0, y: 0, label: "右下" },
  { x: -280, y: 0, label: "左下" },
  { x: 0, y: -200, label: "右上" },
  { x: -280, y: -200, label: "左上" },
];

<DraggableSnapButton snapPoints={corners} aria-label="客服">
  客服
</DraggableSnapButton>

調整回彈手感

springConfig 接受 Motion 的 SpringOptions,降低 stiffness、提高 damping 可以得到比較慢、不帶過衝的吸附。

<DraggableSnapButton
  axis="y"
  snapPoints={[{ y: 0, label: "展開" }, { y: 120, label: "收起" }]}
  springConfig={{ stiffness: 260, damping: 40, mass: 1 }}
>
  拖曳調整
</DraggableSnapButton>

Props

Prop型別預設值說明
snapPointsSnapPoint[][{ x: 0, y: 0 }]落點清單;放開後吸附到最近的一點。預設只有原點即純回彈
axis'both' | 'x' | 'y''both'限制拖曳方向:自由、僅水平、僅垂直
indexnumber—受控目前落點索引;提供時由外部主導位置
defaultIndexnumber0非受控初始落點索引
onSnapChange(index: number, point: SnapPoint) => void—吸附落定到「不同」落點時觸發
springConfigSpringOptions{ stiffness: 520, damping: 30, mass: 0.9 }回彈吸附的 spring 參數
dragElasticnumber | boolean0.16拖出邊界時的橡皮筋彈性,需搭配 dragConstraints
dragConstraintsRefObject | { top, left, right, bottom }—拖曳邊界,透傳給 motion
classNamestring—覆蓋預設樣式
childrenReactNode—按鈕內容

SnapPoint 為 { x?: number; y?: number; label?: string },x/y 是相對按鈕原始位置的位移(px),省略的軸視為 0;label 供輔助科技朗讀。其餘 <button> 屬性(disabled、aria-label 等)皆直接透傳。

細節

  • 拖曳與吸附:以 motion 的 drag 承載位置,dragMomentum 關閉讓落點計算穩定;放開時只計入可拖曳的軸,找出距離最近的落點並以 spring 吸附過去
  • 純回彈:snapPoints 只有原點時,任何方向拖曳放開都彈回原位;搭配 dragConstraints 與 dragElastic 可得到拖出邊界的橡皮筋手感
  • 受控/非受控:提供 index 進入受控模式,位置由外部主導、onSnapChange 只作為請求;否則以 defaultIndex 起始並自行記錄落點
  • 落定通知:只有吸附到「不同」於前一個的落點才會觸發 onSnapChange,回彈到原落點不重複發送
  • 鍵盤支援:多落點時方向鍵(←↑ 上一段、→↓ 下一段)、Home/End 可在落點間切換,並攔下方向鍵的頁面捲動
  • 無障礙:單軸多落點時補上 role="slider" 與 aria-valuemin/max/now/valuetext,等同滑桿語意;另以 aria-describedby 掛上拖曳說明、aria-live="polite" 宣告目前落點
  • 使用者系統開啟「減少動態效果」時:吸附仍完整發生(位置照樣移動,功能不變),僅把 spring 換成極短的線性補間,並移除拖曳時的縮放
  • touch-none + select-none 避免觸控拖曳時觸發捲動與選字;卸載時會停掉進行中的 spring,避免動畫回呼在卸載後才觸發

常見問題

可以在 Next.js App Router 使用嗎?

可以。元件檔案開頭已有 "use client",在 Server Component 頁面中照常 import 即可。位置以 motion value 初始化在起始落點上,掛載後不會跳位。

落點的座標是相對哪裡?

SnapPoint 的 x、y 是相對按鈕原始排版位置的位移(px),省略的軸視為 0。放開時只計入 axis 允許的軸來找最近落點。

怎麼改顏色與尺寸?

傳入 className 覆蓋預設樣式,例如 "bg-blue-600 px-4 py-2 text-white";類別會經 cn() 合併,後面的 Tailwind 類別會蓋掉預設值。

鍵盤與螢幕閱讀器能用嗎?

有多個落點時,方向鍵切換上一段或下一段,Home/End 跳到首尾。單軸多落點時按鈕帶 role="slider" 與對應的 aria-value*,目前落點以 aria-live 播報,名稱取自 label。

會和 onClick 衝突嗎?

其餘 button 屬性會直接透傳,包含 onClick 與 disabled;disabled 時拖曳也會停用。元件只排除了與 Motion 拖曳事件型別衝突的 onDrag、onDragStart、onDragEnd 與 onAnimationStart。