Confetti Feedback
自製粒子彩帶慶祝:從原點噴發、各帶初速/重力/旋轉/飄移,rAF 更新落地後回收。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add confetti-feedbackConfetti Feedback 是用 React 做的彩帶特效(confetti)元件:提供點擊即噴發的 ConfettiButton 與可在任意時機觸發的 useConfetti hook。彩帶是 DOM 小色塊,以 requestAnimationFrame 模擬重力與飄動,透過 createPortal 疊在頁面最上層;Motion 只用來偵測減少動態效果設定。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<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 | 型別 | 預設值 | 說明 |
|---|---|---|---|
particleCount | number | 50 | 每次噴發的粒子數,夾在 0 ~ 80 之間 |
colors | string[] | 內建七色 | 彩帶配色候選,逐片隨機取用 |
onClick | (e) => void | — | 噴發後照常執行 |
className | string | — | 附加到按鈕的樣式 |
useConfetti()
| 回傳 | 型別 | 說明 |
|---|---|---|
fire | (origin?, options?) => void | 觸發一批噴發;origin 為視口座標 { x, y } |
confetti | React.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 的話,從安裝區塊的「手動」分頁複製即可。