A well-designed pricing page is one of the most important pages on a SaaS or service website. Here is how to build one with React and Tailwind CSS.
Step 1: Define Pricing Data
// lib/pricing.ts
export type PricingPlan = {
name: string;
description: string;
monthlyPrice: number;
annualPrice: number;
features: string[];
highlighted: boolean;
cta: string;
href: string;
};
export const plans: PricingPlan[] = [
{
name: "Starter",
description: "For individuals and small projects",
monthlyPrice: 29,
annualPrice: 290,
features: [
"5 pages",
"Basic SEO setup",
"Contact form",
"Mobile responsive",
"1 revision round",
],
highlighted: false,
cta: "Get Started",
href: "/contact?plan=starter",
},
{
name: "Professional",
description: "For growing businesses",
monthlyPrice: 79,
annualPrice: 790,
features: [
"15 pages",
"Advanced SEO",
"CMS integration",
"Analytics setup",
"E-commerce ready",
"3 revision rounds",
"Priority support",
],
highlighted: true,
cta: "Get Started",
href: "/contact?plan=professional",
},
{
name: "Enterprise",
description: "For large organizations",
monthlyPrice: 199,
annualPrice: 1990,
features: [
"Unlimited pages",
"Custom functionality",
"API integrations",
"Performance optimization",
"Security hardening",
"Unlimited revisions",
"Dedicated support",
"SLA guarantee",
],
highlighted: false,
cta: "Contact Sales",
href: "/contact?plan=enterprise",
},
];
Step 2: Build the Billing Toggle
"use client";
import { useState } from "react";
function BillingToggle({
isAnnual,
onToggle,
}: {
isAnnual: boolean;
onToggle: () => void;
}) {
return (
<div className="flex items-center justify-center gap-3">
<span
className={`text-sm font-medium ${!isAnnual ? "text-gray-900 dark:text-white" : "text-gray-500"}`}
>
Monthly
</span>
<button
onClick={onToggle}
className="relative inline-flex h-6 w-11 items-center rounded-full bg-gray-200 transition-colors dark:bg-gray-700"
role="switch"
aria-checked={isAnnual}
>
<span
className={`inline-block h-4 w-4 rounded-full bg-white transition-transform ${
isAnnual ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<span
className={`text-sm font-medium ${isAnnual ? "text-gray-900 dark:text-white" : "text-gray-500"}`}
>
Annual
<span className="ml-1 text-xs text-green-600 font-semibold">
Save 17%
</span>
</span>
</div>
);
}
Step 3: Build the Pricing Card
function PricingCard({
plan,
isAnnual,
}: {
plan: PricingPlan;
isAnnual: boolean;
}) {
const price = isAnnual ? plan.annualPrice : plan.monthlyPrice;
const period = isAnnual ? "/year" : "/month";
return (
<div
className={`relative rounded-2xl p-8 ${
plan.highlighted
? "border-2 border-blue-600 bg-white shadow-xl dark:bg-gray-800"
: "border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
}`}
>
{plan.highlighted && (
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
<span className="rounded-full bg-blue-600 px-4 py-1 text-sm font-medium text-white">
Most Popular
</span>
</div>
)}
<div className="mb-6">
<h3 className="text-xl font-bold text-gray-900 dark:text-white">
{plan.name}
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{plan.description}
</p>
</div>
<div className="mb-6">
<span className="text-4xl font-bold text-gray-900 dark:text-white">
${price}
</span>
<span className="text-gray-500 dark:text-gray-400">{period}</span>
</div>
<a
href={plan.href}
className={`block w-full rounded-lg py-3 text-center font-medium transition-colors ${
plan.highlighted
? "bg-blue-600 text-white hover:bg-blue-700"
: "bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600"
}`}
>
{plan.cta}
</a>
<ul className="mt-8 space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start gap-3">
<svg
className="h-5 w-5 flex-shrink-0 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
<span className="text-sm text-gray-600 dark:text-gray-300">
{feature}
</span>
</li>
))}
</ul>
</div>
);
}
Step 4: Assemble the Pricing Page
"use client";
import { useState } from "react";
import { plans } from "@/lib/pricing";
export default function PricingPage() {
const [isAnnual, setIsAnnual] = useState(false);
return (
<section className="py-20">
<div className="mx-auto max-w-7xl px-6">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-5xl">
Simple, transparent pricing
</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-gray-500 dark:text-gray-400">
Choose the plan that fits your business. No hidden fees. Cancel anytime.
</p>
</div>
<div className="mt-10">
<BillingToggle
isAnnual={isAnnual}
onToggle={() => setIsAnnual(!isAnnual)}
/>
</div>
<div className="mt-12 grid gap-8 lg:grid-cols-3">
{plans.map((plan) => (
<PricingCard key={plan.name} plan={plan} isAnnual={isAnnual} />
))}
</div>
<p className="mt-12 text-center text-sm text-gray-500">
All plans include SSL, hosting, and 30-day money-back guarantee.
</p>
</div>
</section>
);
}
Step 5: Add a Feature Comparison Table
const features = [
{ name: "Custom domain", starter: true, professional: true, enterprise: true },
{ name: "SSL certificate", starter: true, professional: true, enterprise: true },
{ name: "SEO optimization", starter: "Basic", professional: "Advanced", enterprise: "Advanced" },
{ name: "CMS", starter: false, professional: true, enterprise: true },
{ name: "E-commerce", starter: false, professional: true, enterprise: true },
{ name: "API integrations", starter: false, professional: false, enterprise: true },
{ name: "Priority support", starter: false, professional: true, enterprise: true },
{ name: "Custom features", starter: false, professional: false, enterprise: true },
];
function ComparisonTable() {
return (
<div className="mt-20 overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b dark:border-gray-700">
<th className="py-4 pr-6 text-sm font-medium text-gray-500">Feature</th>
<th className="px-6 py-4 text-sm font-medium text-gray-500">Starter</th>
<th className="px-6 py-4 text-sm font-medium text-gray-500">Professional</th>
<th className="px-6 py-4 text-sm font-medium text-gray-500">Enterprise</th>
</tr>
</thead>
<tbody>
{features.map((feature) => (
<tr key={feature.name} className="border-b dark:border-gray-700">
<td className="py-4 pr-6 text-sm text-gray-900 dark:text-white">
{feature.name}
</td>
{[feature.starter, feature.professional, feature.enterprise].map(
(value, i) => (
<td key={i} className="px-6 py-4 text-sm">
{typeof value === "boolean" ? (
value ? (
<span className="text-green-500">Yes</span>
) : (
<span className="text-gray-300">—</span>
)
) : (
<span className="text-gray-600 dark:text-gray-300">{value}</span>
)}
</td>
)
)}
</tr>
))}
</tbody>
</table>
</div>
);
}
Accessibility Considerations
- Use
role="switch"andaria-checkedon the billing toggle - Ensure sufficient color contrast for highlighted plans
- Make the comparison table scrollable on mobile
- Include clear focus styles on interactive elements
Need a Custom Pricing Page?
We design and build conversion-optimized pricing pages for SaaS and service businesses. Contact us to discuss your project.