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.