覆蓋層與回饋

Confetti Feedback

自製粒子彩帶慶祝:從原點噴發、各帶初速/重力/旋轉/飄移,rAF 更新落地後回收。

把這個效果裝進你的專案

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

npx webberui@latest add confetti-feedback

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

Confetti Feedback 是用 React 做的彩帶特效(confetti)元件:提供點擊即噴發的 ConfettiButton 與可在任意時機觸發的 useConfetti hook。彩帶是 DOM 小色塊,以 requestAnimationFrame 模擬重力與飄動,透過 createPortal 疊在頁面最上層;Motion 只用來偵測減少動態效果設定。

Playground

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

50
編輯中的呼叫預設屬性
<ConfettiButton/>

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

適用情境

  • 完成付款、訂閱升級或建立第一個專案後的成功畫面。
  • 新手引導或設定清單全部完成時的回饋。
  • 達成里程碑,例如連續簽到、累積點數、銷售目標達標。
  • 抽獎、填寫問卷或活動報名送出後的慶祝效果。
  • 不適合:只想在點擊位置加一點小火花時,改用 Click Spark。

安裝

npx webberui@latest add https://webberui.com/r/confetti-feedback.json

或在 components.json 設定 registries 後,改用 @webberui/confetti-feedback 安裝。

使用

最簡單的用法是 ConfettiButton,點擊時會從按鈕中心噴發彩帶並照常執行 onClick:

import { ConfettiButton } from "@/components/ui/confetti-feedback";

<ConfettiButton onClick={() => save()}>慶祝一下 🎉</ConfettiButton>

需要在任意時機、從任意座標噴發時,改用 useConfetti hook。它回傳 fire 與必須渲染到畫面上的 confetti 節點:

import { useConfetti } from "@/components/ui/confetti-feedback";

function CheckoutDone() {
  const { fire, confetti } = useConfetti();

  return (
    <>
      <button
        onClick={(e) => {
          const r = e.currentTarget.getBoundingClientRect();
          fire({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
        }}
      >
        完成
      </button>
      {confetti}
    </>
  );
}

fire(origin?, options?) 的 origin 為視口座標(省略時預設畫面上方中央),options 可帶 particleCount 與 colors。

範例

表單送出成功後才噴發

fire 可以在任何非同步流程結束後呼叫。省略 origin 時會從畫面上方三分之一的中央噴發;confetti 節點必須渲染出來才看得到彩帶。

"use client";

import * as React from "react";
import { useConfetti } from "@/components/ui/confetti-feedback";

export function UpgradeForm() {
  const { fire, confetti } = useConfetti();
  const [done, setDone] = React.useState(false);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const res = await fetch("/api/subscription", { method: "POST" });
    if (res.ok) {
      setDone(true);
      fire();
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">升級方案</button>
      {done && <p role="status">已完成升級,新功能現在可以使用了。</p>}
      {confetti}
    </form>
  );
}

使用品牌配色

ConfettiButton 透傳所有原生 button 屬性;colors 傳入配色候選,每片彩帶隨機取用,particleCount 控制數量(最多 80)。

import { ConfettiButton } from "@/components/ui/confetti-feedback";

<ConfettiButton
  colors={["#0ea5e9", "#6366f1", "#f8fafc"]}
  particleCount={70}
  className="bg-indigo-600 hover:bg-indigo-500"
  onClick={() => markAllDone()}
>
  全部標示為完成
</ConfettiButton>

從指定元素的位置噴發

origin 是視口座標,可以用目標元素的 getBoundingClientRect() 算出中心點。下面的例子在進度達到 100% 時,從進度條上方噴發。

"use client";

import * as React from "react";
import { useConfetti } from "@/components/ui/confetti-feedback";

export function GoalProgress({ percent }: { percent: number }) {
  const barRef = React.useRef<HTMLDivElement>(null);
  const { fire, confetti } = useConfetti();

  React.useEffect(() => {
    if (percent < 100 || !barRef.current) return;
    const r = barRef.current.getBoundingClientRect();
    fire({ x: r.left + r.width / 2, y: r.top }, { particleCount: 60 });
  }, [percent, fire]);

  return (
    <>
      <div ref={barRef} className="h-2 rounded-full bg-neutral-200">
        <div className="h-full rounded-full bg-emerald-500" style={{ width: `${percent}%` }} />
      </div>
      {confetti}
    </>
  );
}

Props

ConfettiButton

透傳所有原生 button 屬性,另外提供:

Prop型別預設值說明
particleCountnumber50每次噴發的粒子數,夾在 0 ~ 80 之間
colorsstring[]內建七色彩帶配色候選,逐片隨機取用
onClick(e) => void—噴發後照常執行
classNamestring—附加到按鈕的樣式

useConfetti()

回傳型別說明
fire(origin?, options?) => void觸發一批噴發;origin 為視口座標 { x, y }
confettiReact.ReactNode必須渲染的節點,內部以 createPortal 疊到最上層

options 支援 particleCount(上限 80)與 colors。

細節與可及性

  • 粒子為 DOM 小色塊,rAF 逐幀更新 transform(位移、旋轉、飄移擺動),不觸發 React re-render
  • 每片彩帶帶隨機初速與重力,落到視口底部或超過最長存活時間即回收;整批清空後自動卸載
  • dt 有上限,分頁切回或掉幀時不會讓粒子瞬移
  • 單批粒子上限 80,連點時只保留最新幾批,較舊批次會卸載並停掉其 rAF
  • 卸載時 cancelAnimationFrame 清理,不會在卸載後更新
  • 純裝飾層帶 aria-hidden 且 pointer-events-none,不干擾指標與輔助科技
  • 使用者系統開啟「減少動態效果」時完全不噴發,但按鈕的 onClick 動作仍會執行

常見問題

可以在 Next.js App Router 使用嗎?會有 hydration 問題嗎?

可以。元件檔案開頭已有 "use client"。createPortal 需要 document,所以彩帶層只在掛載後、而且有噴發批次時才渲染,伺服器輸出為空,不會造成 hydration 不符。

會不會影響效能?

每片彩帶是一個 span,由 requestAnimationFrame 直接更新 transform 與 opacity,逐幀動畫不經過 React 重新渲染。單批最多 80 片,同時最多保留 6 批,落出視窗或超過存活時間的彩帶會被回收,整批結束後自動卸載。

彩帶會被 modal 或固定導覽列擋住嗎?

彩帶層以 fixed inset-0 放在 document.body 下,z-index 為 2147483000,並設定 pointer-events-none,一般會疊在其他內容之上,也不會擋住點擊。

開啟減少動態效果時會怎樣?

系統開啟「減少動態效果」時 fire 直接不噴發;ConfettiButton 的 onClick 仍會照常執行,所以成功訊息之類的回饋請另外呈現,不要只靠彩帶。

需要額外的特效套件嗎?

不需要。依賴只有 motion、clsx 與 tailwind-merge,彩帶的物理運算都寫在元件原始碼裡。不用 CLI 的話,從安裝區塊的「手動」分頁複製即可。