Skip to main content
Clever Ops

Bundle Builder Panel

A frequently-bought-together panel for React, structural product thumbs joined by plus glyphs, honest visible checkboxes, preformatted bundle pricing and quiet Tailwind savings type.

Preview & code

Loading Bundle Builder Panel preview

Installation

pnpm dlx shadcn@latest add @cleverui/bundle-builder-panel

Requires the @cleverui registry in your components.json, a one-off, two-line setup.

Usage

tsx
import { BundleBuilderPanel } from '@/components/ui/bundle-builder-panel'
Exampletsx
'use client'

import { useMemo, useState } from 'react'
import { BundleBuilderPanel } from '@/components/ui/bundle-builder-panel'

const ITEMS = [
  {
    id: 'bottle',
    name: 'Traverse Bottle 750',
    price: '$68',
    detail: '750ml, brushed steel',
    locked: true as const,
  },
  {
    id: 'sleeve',
    name: 'Insulated Sleeve',
    price: '$28',
    detail: 'Neoprene, charcoal',
  },
  {
    id: 'filter',
    name: 'Carbon Cap Filter',
    price: '$42',
    detail: '3-pack, food-grade',
  },
  {
    id: 'strap',
    name: 'Carry Strap',
    price: '$24',
    detail: 'Leather, 120cm',
  },
]

/** Preformatted totals keyed by sorted selected ids, consumer owns the math. */
const BUNDLE_TABLE: Record<
  string,
  { bundlePrice: string; compareTotal: string; savingsLabel: string }
> = {
  bottle: {
    bundlePrice: '$68',
    compareTotal: '$68',
    savingsLabel: '',
  },
  'bottle,sleeve': {
    bundlePrice: '$88',
    compareTotal: '$96',
    savingsLabel: 'You save $8',
  },
  'bottle,filter': {
    bundlePrice: '$98',
    compareTotal: '$110',
    savingsLabel: 'You save $12',
  },
  'bottle,strap': {
    bundlePrice: '$84',
    compareTotal: '$92',
    savingsLabel: 'You save $8',
  },
  'bottle,filter,sleeve': {
    bundlePrice: '$118',
    compareTotal: '$138',
    savingsLabel: 'You save $20',
  },
  'bottle,sleeve,strap': {
    bundlePrice: '$108',
    compareTotal: '$120',
    savingsLabel: 'You save $12',
  },
  'bottle,filter,strap': {
    bundlePrice: '$118',
    compareTotal: '$134',
    savingsLabel: 'You save $16',
  },
  'bottle,filter,sleeve,strap': {
    bundlePrice: '$134',
    compareTotal: '$162',
    savingsLabel: 'You save $28',
  },
}

export default function BundleBuilderPanelDemo() {
  const [selected, setSelected] = useState(['bottle', 'sleeve', 'filter'])

  const pricing = useMemo(() => {
    const key = [...new Set(['bottle', ...selected])]
      .sort()
      .join(',')
    return (
      BUNDLE_TABLE[key] ?? {
        bundlePrice: '$134',
        compareTotal: '$162',
        savingsLabel: 'You save $28',
      }
    )
  }, [selected])

  return (
    <div className="mx-auto flex w-full max-w-md justify-center px-4 py-10 sm:px-6 sm:py-14">
      <BundleBuilderPanel
        heading="Complete the kit"
        items={ITEMS}
        selected={selected}
        onToggle={(id, included) => {
          setSelected((prev) => {
            if (included) {
              return prev.includes(id) ? prev : [...prev, id]
            }
            return prev.filter((x) => x !== id)
          })
        }}
        bundlePrice={pricing.bundlePrice}
        compareTotal={
          pricing.compareTotal !== pricing.bundlePrice
            ? pricing.compareTotal
            : undefined
        }
        savingsLabel={pricing.savingsLabel || undefined}
        ctaLabel="Add bundle to cart"
        onAdd={() => {}}
      />
    </div>
  )
}

API reference

BundleBuilderPanel accepts the following props.

PropTypeDefaultDescription
headingstring'Complete the kit'
items*BundleBuilderItem[]
selected*string[]ids currently in the bundle, fully controlled; locked ids always treated as selected
onToggle*(id: string, included: boolean) => void
bundlePrice*stringPREFORMATTED current bundle total, e.g. "$209"
compareTotalstringPREFORMATTED sum of individual prices, e.g. "$247"
savingsLabelstringPREFORMATTED, e.g. "You save $38"
ctaLabelstring'Add bundle to cart'
onAdd(ids: string[]) => void

…plus everything from Omit<React.HTMLAttributes<HTMLElement>, 'onToggle'>: className, event handlers, aria attributes and the rest pass straight through.

Referenced types

Typestsx
export interface BundleBuilderItem {
  id: string
  name: string
  /** PREFORMATTED, e.g. "$68" */
  price: string
  /** e.g. "750ml, brushed steel" */
  detail?: string
  /** anchor item, always included, checkbox disabled */
  locked?: boolean
}

Frequently asked questions

How do I install the Bundle Builder Panel component?

Register the @cleverui registry in your components.json ("@cleverui": "https://cleverops.com.au/r/{name}.json"), then run `npx shadcn@latest add @cleverui/bundle-builder-panel`. The shadcn CLI copies the source into your project, you own the code from that point. You can also copy the source directly from this page.

What dependencies does Bundle Builder Panel require?

Bundle Builder Panel uses motion and lucide-react on top of React and Tailwind CSS. You don't need to install them manually, the shadcn CLI resolves and installs npm dependencies automatically when you add the component.

Can I theme Bundle Builder Panel to match my brand?

Yes. The component is styled entirely with Tailwind utilities over standard design tokens (primary, background, muted, border), so it inherits your existing shadcn/ui theme automatically. Change your CSS variables and the component follows, no source edits needed for palette, radius, or fonts.

Want this built for you?

CleverUI is built by the Clever Ops web team. If you'd rather ship a hand-coded, fast, custom website than assemble one, we build them for Australian businesses every week.

Explore web development services