Pricing Section Component

Learn how to customize the tiered pricing section with billing frequency toggle, configure subscription plans, optimize for conversions, and integrate with Stripe checkout.

Modern Pricing Section Overview

The PricingSection component implements a sophisticated 3-tier subscription system with monthly/yearly billing options, automatic savings calculations, and seamless Stripe checkout integration.

šŸŽÆ Key Features

  • • 3-tier pricing (Free, Pro, Ultra)
  • • Billing frequency toggle (Monthly/Yearly)
  • • Automatic savings calculation
  • • Popular plan highlighting
  • • Feature comparison lists
  • • Stripe checkout integration
  • • Authentication-aware buttons
  • • Loading states and error handling

šŸ’° Conversion Optimization

  • • Yearly billing highlighted by default
  • • Clear savings indicators (17% off)
  • • Crossed-out monthly pricing
  • • Responsive design for all devices
  • • Smooth transitions and animations
  • • Clear value propositions
  • • Professional design patterns
  • • A/B testing ready
šŸ—ļø Subscription Architecture

The pricing section is built on a programmatic, type-safe architecture:

Free Tier

Perfect for getting started

  • • $0 forever
  • • Basic features
  • • Email support
  • • 5 rankings per month

Pro Tier ⭐

Most popular choice

  • • $20/month or $200/year
  • • Advanced analytics
  • • Priority support
  • • API access
  • • Unlimited rankings

Ultra Tier

For teams and agencies

  • • $50/month or $500/year
  • • Enterprise features
  • • Dedicated support
  • • Team collaboration
  • • White-label options

Programmatic Configuration: All pricing is centrally managed insrc/lib/subscriptions.tsusing TypeScript enums and type-safe operations. No hardcoded values anywhere.

šŸ”„ Billing Frequency Toggle

The component includes a sophisticated billing frequency toggle that defaults to yearly billing (higher revenue for you) and shows clear savings indicators.

1// Billing frequency state management
2const [billingFrequency, setBillingFrequency] = useState<BillingFrequency>(
3 BillingFrequency.YEARLY // Defaults to yearly for better conversion
4);
5
6// Dynamic plan selection based on frequency
7const getPlansToShow = () => {
8 const basePlans = [SubscriptionType.FREE];
9
10 if (billingFrequency === BillingFrequency.MONTHLY) {
11 basePlans.push(SubscriptionType.PRO_MONTHLY, SubscriptionType.ULTRA_MONTHLY);
12 } else {
13 basePlans.push(SubscriptionType.PRO_YEARLY, SubscriptionType.ULTRA_YEARLY);
14 }
15
16 return basePlans;
17};

Toggle Features

  • • Yearly highlighted by default
  • • Smooth transition animations
  • • Clear active/inactive states
  • • Professional design

Savings Display

  • • Shows monthly equivalent pricing
  • • Crosses out monthly price
  • • "Save 17%" indicators
  • • Automatic calculation

Basic Implementation

PricingSection Component

The core pricing section with responsive design and conversion optimization:

src/components/PricingSection.tsx
1import { auth } from "~/auth";
2import { db } from "~/server/db";
3import { PricingCard } from "./PricingCard";
4import { SUBSCRIPTION_PLANS } from "~/lib/subscriptions";
5
6export async function PricingSection() {
7 const session = await auth();
8
9 // Get user's current subscription if logged in
10 const currentSubscription = session?.user?.id
11 ? await db.subscription.findUnique({
12 where: { userId: session.user.id },
13 })
14 : null;
15
16 const plans = [
17 {
18 id: "free",
19 name: "Free",
20 price: "$0",
21 description: "Perfect for trying out RankThis",
22 features: [
23 "5 projects per month",
24 "Basic analytics",
25 "Email support",
26 "Standard templates"
27 ],
28 limitations: [
29 "Limited customization",
30 "Basic support only"
31 ],
32 buttonText: "Get Started",
33 buttonVariant: "outline" as const,
34 current: currentSubscription?.type === "FREE",
35 },
36 {
37 id: "monthly",
38 name: "Pro Monthly",
39 price: "$29",
40 priceId: process.env.STRIPE_PRICE_MONTHLY,
41 billing: "/month",
42 description: "For growing businesses and power users",
43 popular: true,
44 features: [
45 "Unlimited projects",
46 "Advanced analytics",
47 "Priority support",
48 "Custom templates",
49 "Team collaboration",
50 "API access",
51 "Advanced integrations"
52 ],
53 buttonText: currentSubscription?.type === "MONTHLY" ? "Current Plan" : "Start Free Trial",
54 buttonVariant: "default" as const,
55 current: currentSubscription?.type === "MONTHLY",
56 },
57 {
58 id: "yearly",
59 name: "Pro Yearly",
60 price: "$290",
61 priceId: process.env.STRIPE_PRICE_YEARLY,
62 billing: "/year",
63 originalPrice: "$348",
64 savings: "Save $58",
65 description: "Best value for committed teams",
66 features: [
67 "Everything in Pro Monthly",
68 "2 months free",
69 "Priority onboarding",
70 "Dedicated account manager",
71 "Custom integrations",
72 "SLA guarantee"
73 ],
74 buttonText: currentSubscription?.type === "YEARLY" ? "Current Plan" : "Start Free Trial",
75 buttonVariant: "default" as const,
76 current: currentSubscription?.type === "YEARLY",
77 }
78 ];
79
80 return (
81 <section className="py-24">
82 <div className="container mx-auto px-4">
83 {/* Header */}
84 <div className="text-center mb-16">
85 <h2 className="text-4xl font-bold mb-4">
86 Simple, Transparent Pricing
87 </h2>
88 <p className="text-xl text-muted-foreground mb-6">
89 Choose the perfect plan for your needs. Start free, upgrade anytime.
90 </p>
91 <div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
92 <span>āœ… 14-day free trial</span>
93 <span>•</span>
94 <span>āœ… Cancel anytime</span>
95 <span>•</span>
96 <span>āœ… No setup fees</span>
97 </div>
98 </div>
99
100 {/* Pricing Cards */}
101 <div className="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto">
102 {plans.map((plan) => (
103 <PricingCard
104 key={plan.id}
105 plan={plan}
106 currentUser={session?.user}
107 isLoggedIn={!!session?.user}
108 />
109 ))}
110 </div>
111
112 {/* Social Proof */}
113 <div className="text-center mt-16">
114 <p className="text-muted-foreground mb-4">
115 Trusted by 1,000+ businesses worldwide
116 </p>
117 <div className="flex items-center justify-center gap-8 opacity-60">
118 {/* Add customer logos here */}
119 <div className="text-2xl font-bold">Company A</div>
120 <div className="text-2xl font-bold">Company B</div>
121 <div className="text-2xl font-bold">Company C</div>
122 </div>
123 </div>
124 </div>
125 </section>
126 );
127}
PricingCard Component

Individual pricing card with Stripe integration and smart button states:

src/components/PricingCard.tsx
1"use client";
2
3import { useState } from "react";
4import { Button } from "~/components/ui/button";
5import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
6import { Badge } from "~/components/ui/badge";
7import { Check, Loader2 } from "lucide-react";
8import { createCheckoutSession } from "~/server/actions/stripe";
9import type { User } from "next-auth";
10
11interface PricingCardProps {
12 plan: {
13 id: string;
14 name: string;
15 price: string;
16 priceId?: string;
17 billing?: string;
18 originalPrice?: string;
19 savings?: string;
20 description: string;
21 popular?: boolean;
22 features: string[];
23 limitations?: string[];
24 buttonText: string;
25 buttonVariant: "default" | "outline";
26 current?: boolean;
27 };
28 currentUser?: User;
29 isLoggedIn: boolean;
30}
31
32export function PricingCard({ plan, currentUser, isLoggedIn }: PricingCardProps) {
33 const [isLoading, setIsLoading] = useState(false);
34
35 const handleSubscribe = async () => {
36 if (!isLoggedIn) {
37 // Redirect to sign in
38 window.location.href = "/auth/signin";
39 return;
40 }
41
42 if (!plan.priceId) {
43 // Free plan - redirect to dashboard
44 window.location.href = "/account";
45 return;
46 }
47
48 setIsLoading(true);
49 try {
50 const result = await createCheckoutSession(plan.priceId);
51
52 if (result.error) {
53 console.error("Checkout error:", result.error);
54 return;
55 }
56
57 if (result.url) {
58 window.location.href = result.url;
59 }
60 } catch (error) {
61 console.error("Subscription error:", error);
62 } finally {
63 setIsLoading(false);
64 }
65 };
66
67 return (
68 <Card className={`relative overflow-hidden ${
69 plan.popular
70 ? "border-primary shadow-lg scale-105"
71 : "border-border"
72 } ${plan.current ? "ring-2 ring-primary" : ""}`}>
73 {plan.popular && (
74 <div className="absolute top-0 left-0 right-0">
75 <div className="bg-primary text-primary-foreground text-center py-2 text-sm font-medium">
76 Most Popular
77 </div>
78 </div>
79 )}
80
81 <CardHeader className={`text-center ${plan.popular ? "pt-12" : "pt-6"}`}>
82 <CardTitle className="text-2xl">{plan.name}</CardTitle>
83 <div className="space-y-2">
84 <div className="flex items-baseline justify-center gap-1">
85 <span className="text-4xl font-bold">{plan.price}</span>
86 {plan.billing && (
87 <span className="text-muted-foreground">{plan.billing}</span>
88 )}
89 </div>
90 {plan.originalPrice && (
91 <div className="flex items-center justify-center gap-2">
92 <span className="text-sm text-muted-foreground line-through">
93 {plan.originalPrice}
94 </span>
95 <Badge variant="secondary" className="text-xs">
96 {plan.savings}
97 </Badge>
98 </div>
99 )}
100 </div>
101 <p className="text-muted-foreground">{plan.description}</p>
102 </CardHeader>
103
104 <CardContent className="space-y-6">
105 {/* Features List */}
106 <ul className="space-y-3">
107 {plan.features.map((feature, index) => (
108 <li key={index} className="flex items-start gap-3">
109 <Check className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
110 <span className="text-sm">{feature}</span>
111 </li>
112 ))}
113 </ul>
114
115 {/* Limitations (for free plan) */}
116 {plan.limitations && (
117 <div className="space-y-2">
118 <h4 className="text-sm font-medium text-muted-foreground">Limitations:</h4>
119 <ul className="space-y-2">
120 {plan.limitations.map((limitation, index) => (
121 <li key={index} className="flex items-start gap-3">
122 <span className="h-5 w-5 text-muted-foreground mt-0.5 flex-shrink-0">Ɨ</span>
123 <span className="text-sm text-muted-foreground">{limitation}</span>
124 </li>
125 ))}
126 </ul>
127 </div>
128 )}
129
130 {/* Action Button */}
131 <Button
132 variant={plan.current ? "outline" : plan.buttonVariant}
133 size="lg"
134 className="w-full"
135 onClick={handleSubscribe}
136 disabled={isLoading || plan.current}
137 >
138 {isLoading ? (
139 <>
140 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
141 Loading...
142 </>
143 ) : plan.current ? (
144 "Current Plan"
145 ) : (
146 plan.buttonText
147 )}
148 </Button>
149
150 {/* Trial Info */}
151 {plan.priceId && !plan.current && (
152 <p className="text-xs text-center text-muted-foreground">
153 Start your 14-day free trial. No credit card required.
154 </p>
155 )}
156 </CardContent>
157 </Card>
158 );
159}

Customization Options

Plan Configuration

Modify subscription plans in the centralized configuration:

src/lib/subscriptions.ts
1// src/lib/subscriptions.ts
2import { SubscriptionType } from "@prisma/client";
3
4export const SUBSCRIPTION_PLANS = {
5 FREE: {
6 name: "Free",
7 description: "Perfect for getting started",
8 priceValue: 0,
9 currency: "USD",
10 interval: null,
11 features: [
12 "5 projects per month",
13 "Basic analytics",
14 "Email support"
15 ],
16 limits: {
17 projects: 5,
18 storage: "1GB",
19 apiCalls: 1000
20 }
21 },
22 MONTHLY: {
23 name: "Pro Monthly",
24 description: "For growing businesses",
25 priceValue: 2900, // $29.00 in cents
26 currency: "USD",
27 interval: "month",
28 stripeProductId: process.env.STRIPE_PRICE_MONTHLY,
29 features: [
30 "Unlimited projects",
31 "Advanced analytics",
32 "Priority support",
33 "Team collaboration"
34 ],
35 limits: {
36 projects: -1, // unlimited
37 storage: "100GB",
38 apiCalls: -1
39 }
40 },
41 YEARLY: {
42 name: "Pro Yearly",
43 description: "Best value for teams",
44 priceValue: 29000, // $290.00 in cents
45 currency: "USD",
46 interval: "year",
47 stripeProductId: process.env.STRIPE_PRICE_YEARLY,
48 savings: {
49 amount: 5800, // $58 savings
50 percentage: 17
51 },
52 features: [
53 "Everything in Pro Monthly",
54 "2 months free",
55 "Priority onboarding",
56 "Dedicated account manager"
57 ],
58 limits: {
59 projects: -1,
60 storage: "1TB",
61 apiCalls: -1
62 }
63 }
64} as const;
65
66// Helper function to get plan by type
67export function getPlan(type: SubscriptionType) {
68 return SUBSCRIPTION_PLANS[type];
69}
70
71// Helper to format prices
72export function formatPrice(priceInCents: number, currency = "USD") {
73 return new Intl.NumberFormat("en-US", {
74 style: "currency",
75 currency,
76 }).format(priceInCents / 100);
77}
Visual Customization

Customize the appearance and messaging:

Pricing customization examples
1// Custom pricing section variants
2export function PricingSectionCompact() {
3 return (
4 <section className="py-16 bg-gradient-to-b from-background to-muted/20">
5 <div className="container mx-auto px-4">
6 <div className="text-center mb-12">
7 <h2 className="text-3xl font-bold mb-4">Choose Your Plan</h2>
8 <p className="text-lg text-muted-foreground">
9 Scale as you grow. Change anytime.
10 </p>
11 </div>
12
13 {/* Horizontal layout for mobile */}
14 <div className="flex flex-col md:flex-row gap-6 max-w-4xl mx-auto">
15 {/* Plans here */}
16 </div>
17 </div>
18 </section>
19 );
20}
21
22// Pricing toggle for monthly/yearly
23export function PricingToggle() {
24 const [isYearly, setIsYearly] = useState(false);
25
26 return (
27 <div className="flex items-center justify-center gap-4 mb-8">
28 <span className={`text-sm ${!isYearly ? "font-medium" : "text-muted-foreground"}`}>
29 Monthly
30 </span>
31 <button
32 onClick={() => setIsYearly(!isYearly)}
33 className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
34 isYearly ? "bg-primary" : "bg-gray-200"
35 }`}
36 >
37 <span
38 className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
39 isYearly ? "translate-x-6" : "translate-x-1"
40 }`}
41 />
42 </button>
43 <span className={`text-sm ${isYearly ? "font-medium" : "text-muted-foreground"}`}>
44 Yearly
45 <Badge variant="secondary" className="ml-2 text-xs">
46 Save 17%
47 </Badge>
48 </span>
49 </div>
50 );
51}
52
53// Pricing FAQ section
54export function PricingFAQ() {
55 const faqs = [
56 {
57 question: "Can I change plans anytime?",
58 answer: "Yes! You can upgrade, downgrade, or cancel your subscription at any time from your account settings."
59 },
60 {
61 question: "What happens to my data if I cancel?",
62 answer: "Your data remains accessible for 30 days after cancellation. You can export or reactivate anytime."
63 },
64 {
65 question: "Do you offer refunds?",
66 answer: "We offer a 30-day money-back guarantee for all paid plans. No questions asked."
67 }
68 ];
69
70 return (
71 <div className="mt-24">
72 <h3 className="text-2xl font-bold text-center mb-12">
73 Frequently Asked Questions
74 </h3>
75 <div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
76 {faqs.map((faq, index) => (
77 <div key={index} className="space-y-2">
78 <h4 className="font-medium">{faq.question}</h4>
79 <p className="text-sm text-muted-foreground">{faq.answer}</p>
80 </div>
81 ))}
82 </div>
83 </div>
84 );
85}

Stripe Integration

Checkout Flow

Handle Stripe checkout sessions securely:

src/server/actions/stripe.ts
1// src/server/actions/stripe.ts
2"use server";
3
4import { auth } from "~/auth";
5import { stripe } from "~/server/lib/stripe";
6import { db } from "~/server/db";
7import { env } from "~/env";
8
9export async function createCheckoutSession(priceId: string) {
10 const session = await auth();
11
12 if (!session?.user?.email) {
13 return { error: "Not authenticated" };
14 }
15
16 try {
17 // Get or create Stripe customer
18 let customer = await stripe.customers.list({
19 email: session.user.email,
20 limit: 1
21 });
22
23 let customerId: string;
24 if (customer.data.length === 0) {
25 const newCustomer = await stripe.customers.create({
26 email: session.user.email,
27 name: session.user.name ?? undefined,
28 metadata: {
29 userId: session.user.id
30 }
31 });
32 customerId = newCustomer.id;
33
34 // Update user with Stripe customer ID
35 await db.user.update({
36 where: { id: session.user.id },
37 data: { customerId }
38 });
39 } else {
40 customerId = customer.data[0]!.id;
41 }
42
43 // Create Stripe Checkout session
44 const checkoutSession = await stripe.checkout.sessions.create({
45 customer: customerId,
46 payment_method_types: ["card"],
47 billing_address_collection: "required",
48 line_items: [
49 {
50 price: priceId,
51 quantity: 1,
52 },
53 ],
54 mode: "subscription",
55 allow_promotion_codes: true,
56 subscription_data: {
57 trial_period_days: 14, // 14-day free trial
58 metadata: {
59 userId: session.user.id,
60 },
61 },
62 success_url: `${env.NEXTAUTH_URL}/account?session_id={CHECKOUT_SESSION_ID}`,
63 cancel_url: `${env.NEXTAUTH_URL}/pricing?canceled=true`,
64 metadata: {
65 userId: session.user.id,
66 },
67 });
68
69 return { url: checkoutSession.url };
70 } catch (error) {
71 console.error("Stripe checkout error:", error);
72 return { error: "Failed to create checkout session" };
73 }
74}
Usage in Pages

Add the pricing section to your pages:

Page usage examples
1// src/app/pricing/page.tsx
2import { PricingSection } from "~/components/PricingSection";
3import { PricingFAQ } from "~/components/PricingFAQ";
4
5export default function PricingPage() {
6 return (
7 <main className="min-h-screen">
8 <PricingSection />
9 <PricingFAQ />
10 </main>
11 );
12}
13
14// src/app/page.tsx (Landing page)
15import { PricingSection } from "~/components/PricingSection";
16
17export default function HomePage() {
18 return (
19 <main>
20 {/* Hero section */}
21 <section className="py-24">
22 <h1>Welcome to RankThis</h1>
23 {/* Hero content */}
24 </section>
25
26 {/* Features */}
27 <section className="py-24">
28 {/* Features content */}
29 </section>
30
31 {/* Pricing */}
32 <PricingSection />
33
34 {/* CTA */}
35 <section className="py-24">
36 {/* Final CTA */}
37 </section>
38 </main>
39 );
40}
šŸ’” Pricing Best Practices

Psychology & Design

  • • Use odd numbers ($29 vs $30)
  • • Highlight most popular plan
  • • Show savings on annual plans
  • • Include social proof
  • • Use scarcity carefully

Technical

  • • Handle loading states
  • • Validate on server-side
  • • Test checkout flow thoroughly
  • • Monitor conversion rates
  • • A/B test pricing strategies
🧪 Testing Your Pricing

Stripe Test Mode

Use test card numbers: 4242 4242 4242 4242 (Visa), 4000 0025 0000 3155 (requires authentication)

Conversion Testing

Analytics tracking
1// Track pricing page conversions
2import { trackEvent } from "~/lib/analytics";
3
4// When user views pricing
5trackEvent("pricing_viewed", {
6 source: "landing_page",
7 user_id: session?.user?.id
8});
9
10// When user clicks subscribe
11trackEvent("subscribe_clicked", {
12 plan: planId,
13 price: planPrice,
14 user_id: session?.user?.id
15});
16
17// When checkout completes
18trackEvent("subscription_created", {
19 plan: planId,
20 revenue: planPrice,
21 user_id: session.user.id
22});

Pricing Component Ready!

Your pricing section is configured for maximum conversions. Next, learn about theme toggle functionality.