Staaarter

Billing Per Seat Pricing

A per-seat pricing calculator with a seat count stepper that recalculates the total on every change, tiered volume pricing that drops the per-seat rate as the count grows, and a monthly/yearly toggle with a built-in annual discount. Built for team-focused SaaS pricing pages where letting someone estimate their own cost, before they talk to sales, removes friction from the signup decision.

By Staaarter Team
Billing
Updated June 14, 2026
Docs
Live preview

Docs

Installation

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

npx shadcn add https://staaarter.com/r/billing-per-seat-pricing.json

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

Usage

The file also exports billing93Demo, 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 { Billing93, billing93Demo } from "@/components/blocks/billing/billing93";

export default function Page() {
  return <Billing93 {...billing93Demo} />;
}

Props

PropTypeDefaultDescription
headingstring"Estimate your team's cost"Heading shown above the toggle.
descriptionstringnoneOptional supporting text under the heading.
tiersSeatPricingTier[]noneVolume pricing tiers. The active tier is the one with the highest minSeats that's still at or below the current seat count.
minSeatsnumber1Lower bound for the stepper; the minus button disables at this value.
maxSeatsnumber100Upper bound for the stepper; at this value the CTA is replaced with a "Talk to sales" link.
defaultSeatsnumber5Seat count on first render, clamped to [minSeats, maxSeats].
annualDiscountPercentnumber20Percentage knocked off the annualized total when the yearly toggle is active.
currencySymbolstring"$"Symbol prefixed to every formatted amount.
enterpriseHrefstring"#"Link target for the "Talk to sales" link shown once seats hits maxSeats.
ctaLabelstring"Update seats"Label for the primary button, shown while seats is below maxSeats.
classNamestringnoneExtra classes for the outer card.

Types

interface SeatPricingTier {
  /** Seat count at which this tier's price kicks in. The tier list should be sorted ascending. */
  minSeats: number;
  pricePerSeat: number;
}

Behavior notes

  • Seat count is local state, changeable with the stepper buttons or by typing directly into the number input; every change is clamped to [minSeats, maxSeats] and immediately recalculates the price.
  • The active pricing tier is picked by scanning the tiers array for the highest minSeats that's still at or below the current seat count, so tiers don't need to be pre-sorted, though ascending order is clearer to read.
  • Switching the monthly/yearly toggle recomputes the displayed total using annualDiscountPercent off the annualized monthly rate; it doesn't change the seat count or the active tier.
  • Once seats reaches maxSeats, the primary CTA button is replaced by a "Talk to sales" link instead of staying disabled, since a hard seat ceiling usually means routing to a sales conversation.
  • All money values are computed from the numeric pricePerSeat and seats, not passed in as pre-formatted strings, so the component owns its own currency formatting via currencySymbol.

Frequently asked questions