WebberUI

Bundle Builder

組合搭售機:點選商品縮圖以 FLIP 飛入插槽,總價滾動更新,集滿指定件數即填滿折扣環並解鎖折扣。

載入預覽⋯

Playground

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

3
3
0.2
0
<BundleBuilder />

安裝

npx shadcn@latest add https://webberui.com/r/bundle-builder.json

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

安裝依賴後,從 registry JSON(/r/bundle-builder.jsonfiles[0].content)複製 bundle-builder.tsx 原始碼到你的 components/ui/ 目錄:

npm install motion lucide-react clsx tailwind-merge

使用

import {
  BundleBuilder,
  type BundleProduct,
} from "@/components/ui/bundle-builder";

const products: BundleProduct[] = [
  { id: "a", name: "可頌", price: 65, thumbnail: "🥐" },
  { id: "b", name: "拿鐵", price: 120, thumbnail: "☕" },
  { id: "c", name: "沙拉", price: 150, thumbnail: "🥗" },
  { id: "d", name: "蛋糕", price: 95, thumbnail: "🍰" },
];

<BundleBuilder
  products={products}
  columns={4}
  slots={3}
  discount={0.2}
  currency="NT$"
  onAddToBundle={(items, total) => console.log(items, total)}
/>;

點選上方商品縮圖,縮圖會以 FLIP 動畫飛入下方插槽,插槽集滿設定的件數時,折扣環填滿並彈出折扣標籤、總價滾動至折後價。再次點選商品或插槽即可移除。

受控模式

傳入 selectedIds 即進入受控模式,改由外部狀態主導選取內容:

const [ids, setIds] = React.useState<string[]>([]);

<BundleBuilder
  products={products}
  selectedIds={ids}
  onSelectionChange={setIds}
  onUnlock={() => console.log("折扣已解鎖")}
/>;

Props

Prop型別預設值說明
productsBundleProduct[]商品格要顯示的商品清單
slotsnumber3需集滿的插槽數,達成後解鎖折扣
columnsnumber3商品格欄數
discountnumber0.2集滿後套用的折扣比例(0–1)
currencystring"$"貨幣符號
localestring"en-US"數字千分位所用地區設定
decimalsnumber0價格顯示小數位數
selectedIdsstring[]受控:目前選取的商品 id(依加入順序)
defaultSelectedIdsstring[]非受控模式的初始選取
onSelectionChange(ids: string[]) => void選取變更時觸發
onUnlock() => void由未集滿轉為集滿、折扣解鎖時觸發一次
onAddToBundle(products, total) => void點擊主要按鈕時觸發,回傳選取商品與折後總價
actionLabelReact.ReactNode"加入組合"主要按鈕文字
classNamestring外層容器樣式

BundleProduct

欄位型別說明
idstring唯一識別碼,用於受控與 FLIP 對應
namestring商品名稱
pricenumber單價(未折扣)
thumbnailReact.ReactNode縮圖內容,可放 emoji、圖示或任意節點
thumbnailClassNamestring覆寫縮圖背景(Tailwind class)

細節

  • FLIP 飛入:以 Motion 的 layoutId 共享版面動畫實作。選取後同一縮圖從商品格移到插槽,Motion 自動量測前後位置做 FLIP 過場;格內留下打勾佔位以維持版面穩定。
  • 滾動總價:以 useMotionValue + animate 平滑滾動到新價,逐格更新不觸發 React re-render;集滿時套用折扣並顯示刪除線原價與省下金額。
  • 折扣環:SVG 環形進度隨集滿件數填滿;達標時環轉為強調色並彈出打勾與折扣百分比。
  • 插槽已滿時,未選取的商品會停用;點選插槽或已選商品即可移除。

可及性

  • 商品為原生 button,帶 aria-pressed 標示選取狀態,aria-label 含名稱與價格。
  • 插槽移除鍵帶「從組合移除 ⋯」的 aria-label;折扣環為裝飾性內容並標記 aria-hidden
  • 選取進度與解鎖狀態透過 role="status"aria-live 區域即時播報。
  • 使用者系統開啟「減少動態效果」時,FLIP、滾動數字與折扣環改為瞬間定位,不做過場動畫。

On this page