Skip to main content
Back to Blog
Tutorials
3 min read
November 18, 2024

How to Build a Pricing Calculator in React

Build an interactive pricing calculator with sliders, feature toggles, and real-time cost estimation in React.

Ryel Banfield

Founder & Lead Developer

An interactive pricing calculator helps prospects understand costs before contacting sales. Here is how to build one.

Step 1: Pricing Configuration

// lib/pricing.ts
export interface PricingTier {
  name: string;
  basePrice: number;
  features: string[];
  limits: {
    users: number;
    storage: number; // GB
    apiCalls: number;
  };
}

export const tiers: PricingTier[] = [
  {
    name: "Starter",
    basePrice: 29,
    features: ["5 team members", "10 GB storage", "10K API calls/mo"],
    limits: { users: 5, storage: 10, apiCalls: 10000 },
  },
  {
    name: "Pro",
    basePrice: 79,
    features: ["25 team members", "100 GB storage", "100K API calls/mo"],
    limits: { users: 25, storage: 100, apiCalls: 100000 },
  },
  {
    name: "Enterprise",
    basePrice: 199,
    features: ["Unlimited members", "1 TB storage", "Unlimited API calls"],
    limits: { users: Infinity, storage: 1000, apiCalls: Infinity },
  },
];

export interface AddOn {
  id: string;
  name: string;
  description: string;
  price: number;
  unit: string;
}

export const addOns: AddOn[] = [
  { id: "sso", name: "SSO / SAML", description: "Enterprise single sign-on", price: 49, unit: "/mo" },
  { id: "sla", name: "99.99% SLA", description: "Premium uptime guarantee", price: 99, unit: "/mo" },
  { id: "support", name: "Priority Support", description: "4-hour response time", price: 79, unit: "/mo" },
  { id: "backup", name: "Daily Backups", description: "Automated daily backups", price: 29, unit: "/mo" },
];

export function calculatePrice(
  tier: PricingTier,
  extraUsers: number,
  extraStorage: number,
  selectedAddOns: string[],
  isAnnual: boolean
): number {
  let monthly = tier.basePrice;

  // Extra users: $5/user/mo
  if (extraUsers > 0) monthly += extraUsers * 5;

  // Extra storage: $2/10GB/mo
  if (extraStorage > 0) monthly += Math.ceil(extraStorage / 10) * 2;

  // Add-ons
  for (const addOnId of selectedAddOns) {
    const addOn = addOns.find((a) => a.id === addOnId);
    if (addOn) monthly += addOn.price;
  }

  // Annual discount: 20% off
  if (isAnnual) monthly *= 0.8;

  return monthly;
}

Step 2: Pricing Calculator Component

"use client";

import { useState, useMemo } from "react";
import { tiers, addOns, calculatePrice } from "@/lib/pricing";
import { Check, Minus, Plus } from "lucide-react";

export function PricingCalculator() {
  const [selectedTier, setSelectedTier] = useState(1); // Pro
  const [extraUsers, setExtraUsers] = useState(0);
  const [extraStorage, setExtraStorage] = useState(0);
  const [selectedAddOns, setSelectedAddOns] = useState<string[]>([]);
  const [isAnnual, setIsAnnual] = useState(true);

  const tier = tiers[selectedTier];

  const price = useMemo(
    () => calculatePrice(tier, extraUsers, extraStorage, selectedAddOns, isAnnual),
    [tier, extraUsers, extraStorage, selectedAddOns, isAnnual]
  );

  function toggleAddOn(id: string) {
    setSelectedAddOns((prev) =>
      prev.includes(id) ? prev.filter((a) => a !== id) : [...prev, id]
    );
  }

  return (
    <div className="mx-auto max-w-3xl">
      {/* Billing Toggle */}
      <div className="mb-8 flex items-center justify-center gap-3">
        <span className={!isAnnual ? "font-semibold" : "text-gray-500"}>
          Monthly
        </span>
        <button
          onClick={() => setIsAnnual(!isAnnual)}
          className={`relative h-6 w-11 rounded-full transition-colors ${
            isAnnual ? "bg-blue-600" : "bg-gray-300"
          }`}
        >
          <span
            className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${
              isAnnual ? "translate-x-5" : "translate-x-0.5"
            }`}
          />
        </button>
        <span className={isAnnual ? "font-semibold" : "text-gray-500"}>
          Annual
          <span className="ml-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
            Save 20%
          </span>
        </span>
      </div>

      {/* Tier Selection */}
      <div className="grid grid-cols-3 gap-3">
        {tiers.map((t, i) => (
          <button
            key={t.name}
            onClick={() => setSelectedTier(i)}
            className={`rounded-xl border p-4 text-left transition-all ${
              selectedTier === i
                ? "border-blue-500 bg-blue-50 ring-2 ring-blue-500 dark:bg-blue-950"
                : "border-gray-200 hover:border-gray-300 dark:border-gray-700"
            }`}
          >
            <p className="font-semibold">{t.name}</p>
            <p className="mt-1 text-2xl font-bold">
              ${isAnnual ? Math.round(t.basePrice * 0.8) : t.basePrice}
              <span className="text-sm font-normal text-gray-500">/mo</span>
            </p>
          </button>
        ))}
      </div>

      {/* Customization */}
      <div className="mt-8 space-y-6">
        {/* Extra Users Slider */}
        <div>
          <div className="flex items-center justify-between">
            <label className="text-sm font-medium">Extra Team Members</label>
            <span className="text-sm text-gray-500">
              +{extraUsers} ({tier.limits.users + extraUsers} total) — $
              {extraUsers * 5}/mo
            </span>
          </div>
          <input
            type="range"
            min={0}
            max={100}
            value={extraUsers}
            onChange={(e) => setExtraUsers(Number(e.target.value))}
            className="mt-2 w-full accent-blue-600"
          />
        </div>

        {/* Extra Storage Slider */}
        <div>
          <div className="flex items-center justify-between">
            <label className="text-sm font-medium">Extra Storage</label>
            <span className="text-sm text-gray-500">
              +{extraStorage} GB ({tier.limits.storage + extraStorage} GB total) — $
              {Math.ceil(extraStorage / 10) * 2}/mo
            </span>
          </div>
          <input
            type="range"
            min={0}
            max={500}
            step={10}
            value={extraStorage}
            onChange={(e) => setExtraStorage(Number(e.target.value))}
            className="mt-2 w-full accent-blue-600"
          />
        </div>

        {/* Add-ons */}
        <div>
          <h3 className="mb-3 text-sm font-medium">Add-ons</h3>
          <div className="grid grid-cols-2 gap-3">
            {addOns.map((addon) => (
              <button
                key={addon.id}
                onClick={() => toggleAddOn(addon.id)}
                className={`flex items-start gap-3 rounded-lg border p-3 text-left transition-all ${
                  selectedAddOns.includes(addon.id)
                    ? "border-blue-500 bg-blue-50 dark:bg-blue-950"
                    : "border-gray-200 dark:border-gray-700"
                }`}
              >
                <div
                  className={`mt-0.5 flex h-4 w-4 items-center justify-center rounded border ${
                    selectedAddOns.includes(addon.id)
                      ? "border-blue-500 bg-blue-500"
                      : "border-gray-300"
                  }`}
                >
                  {selectedAddOns.includes(addon.id) && (
                    <Check className="h-3 w-3 text-white" />
                  )}
                </div>
                <div>
                  <p className="text-sm font-medium">{addon.name}</p>
                  <p className="text-xs text-gray-500">{addon.description}</p>
                  <p className="mt-1 text-xs font-semibold text-blue-600">
                    +${addon.price}{addon.unit}
                  </p>
                </div>
              </button>
            ))}
          </div>
        </div>
      </div>

      {/* Total */}
      <div className="mt-8 rounded-xl border-2 border-blue-500 bg-blue-50 p-6 dark:bg-blue-950">
        <div className="flex items-center justify-between">
          <div>
            <p className="text-sm text-gray-500">Estimated Monthly Cost</p>
            <p className="text-4xl font-bold">
              ${Math.round(price)}
              <span className="text-lg font-normal text-gray-500">/mo</span>
            </p>
            {isAnnual && (
              <p className="mt-1 text-sm text-gray-500">
                Billed ${Math.round(price * 12)}/year
              </p>
            )}
          </div>
          <a
            href="/contact"
            className="rounded-lg bg-blue-600 px-6 py-3 font-medium text-white hover:bg-blue-700"
          >
            Get Started
          </a>
        </div>
      </div>
    </div>
  );
}

Need a Custom Pricing Page?

We design and build interactive pricing pages that convert visitors into customers. Contact us to discuss your project.

pricingcalculatorinteractiveReactSaaStutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles