Transparent usage pricing
Pay only for what you use. No hidden fees.
5 users
150+
100 GB
10 GB1 TB
MonthlyYearly -20%
Estimated Cost
$123/month
Billed $1478 yearly
Base Platform$29
Users (5 × $15)$75
Storage (100GB × $0.5)$50
Yearly Discount (20%)-$370
Features
- ✅ Interactive Sliders - Let users estimate costs based on their actual usage
- ✅ Real-time Updates - Price updates instantly as inputs change
- ✅ Annual Savings Toggle - Highlight the discount for yearly billing
- ✅ Transparent Breakdown - Show exactly how the cost is calculated
- ✅ Contact Sales Trigger - Logic to handle high-volume enterprise use cases
- ✅ Smooth Animations - Numbers transition smoothly using
framer-motion - ✅ Mobile Responsive - Works perfectly on all device sizes
Installation
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Check, HelpCircle, Info, Minus, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
interface PricingTier {
name: string;
price: number;
description: string;
features: string[];
highlight?: boolean;
}
interface PricingCalculatorProps {
headline?: string;
description?: string;
currency?: string;
basePrice?: number;
plans?: PricingTier[];
className?: string;
}
export function PricingCalculator({
headline = "Estimate your costs",
description = "Transparent pricing that scales with you.",
currency = "$",
basePrice = 0,
className,
}: PricingCalculatorProps) {
const [users, setUsers] = React.useState(5);
const [storage, setStorage] = React.useState(100);
const [isAnnual, setIsAnnual] = React.useState(true);
// Pricing logic (example)
const userPrice = 15;
const storagePrice = 0.5;
const monthlyTotal = basePrice + users * userPrice + storage * storagePrice;
const annualTotal = monthlyTotal * 12 * 0.8; // 20% discount
const displayedMonthly = isAnnual ? annualTotal / 12 : monthlyTotal;
return (
<section className={cn("w-full py-12 sm:py-16 lg:py-20", className)}>
<div className="container px-4 md:px-6">
<div className="mx-auto max-w-3xl space-y-4 text-center">
<h2 className="text-3xl font-bold tracking-tighter text-foreground sm:text-4xl md:text-5xl">
{headline}
</h2>
{description && (
<p className="text-lg text-muted-foreground md:text-xl">
{description}
</p>
)}
</div>
<div className="mx-auto mt-12 grid max-w-5xl gap-8 lg:grid-cols-2">
{/* Controls */}
<div className="space-y-8 rounded-2xl border border-border bg-card p-8 shadow-sm">
<div className="space-y-6">
{/* Users Slider */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">
Team Members
</label>
<span className="rounded-md bg-muted px-2 py-1 text-sm font-mono font-medium text-foreground">
{users} users
</span>
</div>
<input
type="range"
min="1"
max="50"
step="1"
value={users}
onChange={(e) => setUsers(Number(e.target.value))}
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-muted accent-primary"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>1</span>
<span>50+</span>
</div>
</div>
{/* Storage Slider */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">
Storage (GB)
</label>
<span className="rounded-md bg-muted px-2 py-1 text-sm font-mono font-medium text-foreground">
{storage} GB
</span>
</div>
<input
type="range"
min="10"
max="1000"
step="10"
value={storage}
onChange={(e) => setStorage(Number(e.target.value))}
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-muted accent-primary"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>10 GB</span>
<span>1 TB</span>
</div>
</div>
{/* Billing Toggle */}
<div className="flex items-center justify-center space-x-4 pt-4">
<span
className={cn(
"text-sm",
!isAnnual
? "font-semibold text-foreground"
: "text-muted-foreground",
)}
>
Monthly
</span>
<button
onClick={() => setIsAnnual(!isAnnual)}
className={cn(
"relative h-7 w-12 rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2",
isAnnual ? "bg-primary" : "bg-muted",
)}
>
<motion.div
className="absolute top-1 h-5 w-5 rounded-full bg-white shadow-sm"
animate={{ left: isAnnual ? "26px" : "2px" }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
<span
className={cn(
"text-sm",
isAnnual
? "font-semibold text-foreground"
: "text-muted-foreground",
)}
>
Yearly{" "}
<span className="ml-1.5 rounded-full bg-green-500/10 px-2 py-0.5 text-xs font-medium text-green-600 dark:text-green-400">
-20%
</span>
</span>
</div>
</div>
</div>
{/* Summary Card */}
<div className="relative overflow-hidden rounded-2xl border border-primary/20 bg-primary/5 p-8">
<div className="absolute -right-20 -top-20 h-64 w-64 rounded-full bg-primary/10 blur-3xl" />
<div className="absolute -bottom-20 -left-20 h-64 w-64 rounded-full bg-purple-500/10 blur-3xl" />
<div className="relative flex h-full flex-col justify-between space-y-8">
<div>
<h3 className="text-lg font-medium text-foreground">
Estimated Cost
</h3>
<div className="mt-4 flex items-baseline">
<span className="text-5xl font-bold tracking-tight text-foreground">
{currency}
<AnimatePresence mode="popLayout">
<motion.span
key={Math.round(displayedMonthly)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="inline-block"
>
{Math.round(displayedMonthly)}
</motion.span>
</AnimatePresence>
</span>
<span className="ml-2 text-muted-foreground">/month</span>
</div>
{isAnnual && (
<p className="mt-2 text-sm text-muted-foreground">
Billed {currency}
{Math.round(annualTotal)} yearly
</p>
)}
</div>
<div className="space-y-4">
<div className="flex justify-between border-b border-primary/10 pb-4 text-sm">
<span className="text-muted-foreground">Base Platform</span>
<span className="font-medium">
{currency}
{basePrice}
</span>
</div>
<div className="flex justify-between border-b border-primary/10 pb-4 text-sm">
<span className="text-muted-foreground">
Users ({users} × {currency}
{userPrice})
</span>
<span className="font-medium">
{currency}
{users * userPrice}
</span>
</div>
<div className="flex justify-between border-b border-primary/10 pb-4 text-sm">
<span className="text-muted-foreground">
Storage ({storage}GB × {currency}
{storagePrice})
</span>
<span className="font-medium">
{currency}
{storage * storagePrice}
</span>
</div>
{isAnnual && (
<div className="flex justify-between text-sm text-green-600 dark:text-green-400">
<span className="font-medium">Yearly Discount (20%)</span>
<span className="font-medium">
-{currency}
{Math.round(monthlyTotal * 12 - annualTotal)}
</span>
</div>
)}
</div>
<button className="w-full rounded-lg bg-primary px-4 py-3 text-sm font-bold text-primary-foreground shadow-lg transition-transform hover:scale-[1.02] active:scale-[0.98]">
Start Free Trial
</button>
</div>
</div>
</div>
</div>
</section>
);
}Update the import paths to match your project setup.
Usage
import { PricingCalculator } from "@/components/ui/pricing-calculator";
export default function Page() {
return (
<PricingCalculator
headline="Estimate your costs"
description="Pay only for what you use."
basePrice={29}
currency="$"
/>
);
}Props
| Prop | Type | Description |
|---|---|---|
headline | string | Section headline |
description | string | Section description |
currency | string | Currency symbol (default: "$") |
basePrice | number | Base monthly price before usage costs |
className | string | Additional CSS classes |
Conversion Psychology
Why this converts
- Transparency Builds Trust: Hiding pricing behind "Contact Sales" often leads to drop-offs. A calculator shows you have nothing to hide.
- Ownership Bias: By interacting with the sliders, users start to "build" their own plan, creating a sense of ownership before they even sign up.
- Anchoring: The "Yearly" toggle usually shows a lower monthly equivalent, anchoring the user to the savings rather than the total cost.
- Reduced Friction: Users get an immediate answer to "how much will this cost me?" without needing to do mental math or talk to a human.