Staaarter

Billing Bulk Discount Table

A bulk discount table for B2B pricing pages, showing how the per-seat price drops as a team's seat count grows across defined tiers. Each row lists a tier name, seat range, per-seat price, and savings percentage relative to the base tier, with the row matching the account's current seat count highlighted automatically.

By Staaarter Team
Billing
Updated February 26, 2026
Docs
Live preview

Docs

Installation

A real shadcn registry command, not a copy-paste stand-in: it fetches this block plus its badge dependencies and any missing npm packages, and installs them straight into your project.

npx shadcn add https://staaarter.com/r/billing-bulk-discount-table.json

Prefer not to use the CLI? Copy the source from the Code toggle above into src/components/blocks/billing/billing29.tsx instead.

Usage

The file also exports billing29Demo, the exact props behind the preview above. Spread it to get a working section in one line, then replace it with your own data.

import { Billing29, billing29Demo } from "@/components/blocks/billing/billing29";

export default function Page() {
  return <Billing29 {...billing29Demo} />;
}

Props

PropTypeDefaultDescription
headingstring"Volume discounts"Heading in the card header.
descriptionstringnoneOptional supporting line under the heading.
tiersDiscountTier[]nonePricing tiers rendered in the order given, one row per tier.
currentSeatCountnumbernoneWhen set, the tier whose seat range contains this count gets a "Current" badge and a tinted row.
footerNotestringnoneOptional fine-print line under the table.
classNamestringnoneExtra classes for the outer container.

Types

interface DiscountTier {
  label: string;
  /** Human-readable range shown in the table, e.g. "1 to 10 seats". */
  seatRangeLabel: string;
  minSeats: number;
  /** Omit for the top, open-ended tier. */
  maxSeats?: number;
  pricePerSeat: string;
  savingsPercent?: number;
}

Behavior notes

  • The current-tier match is computed from minSeats/maxSeats on each tier against currentSeatCount, not from the seatRangeLabel string, so the label can say anything while the highlight logic stays accurate.
  • Leaving maxSeats undefined on a tier makes it open-ended, any currentSeatCount at or above that tier's minSeats matches it. That's meant for a top "201+ seats" or "Enterprise" row.
  • If currentSeatCount is omitted entirely, no row is highlighted and no Current badge is shown, the table just displays the plain pricing grid.
  • This uses volume pricing semantics, the whole seat count is billed at the matched tier's rate, not a tiered/graduated model where different seats within one order are billed at different rates.

Frequently asked questions