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.
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
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.
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 management2const [billingFrequency, setBillingFrequency] = useState<BillingFrequency>(3 BillingFrequency.YEARLY // Defaults to yearly for better conversion4);56// Dynamic plan selection based on frequency7const getPlansToShow = () => {8 const basePlans = [SubscriptionType.FREE];910 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 }1516 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
The core pricing section with responsive design and conversion optimization:
1import { auth } from "~/auth";2import { db } from "~/server/db";3import { PricingCard } from "./PricingCard";4import { SUBSCRIPTION_PLANS } from "~/lib/subscriptions";56export async function PricingSection() {7 const session = await auth();89 // Get user's current subscription if logged in10 const currentSubscription = session?.user?.id11 ? await db.subscription.findUnique({12 where: { userId: session.user.id },13 })14 : null;1516 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 ];7980 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 Pricing87 </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>99100 {/* Pricing Cards */}101 <div className="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto">102 {plans.map((plan) => (103 <PricingCard104 key={plan.id}105 plan={plan}106 currentUser={session?.user}107 isLoggedIn={!!session?.user}108 />109 ))}110 </div>111112 {/* Social Proof */}113 <div className="text-center mt-16">114 <p className="text-muted-foreground mb-4">115 Trusted by 1,000+ businesses worldwide116 </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}
Individual pricing card with Stripe integration and smart button states:
1"use client";23import { 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";1011interface 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}3132export function PricingCard({ plan, currentUser, isLoggedIn }: PricingCardProps) {33 const [isLoading, setIsLoading] = useState(false);3435 const handleSubscribe = async () => {36 if (!isLoggedIn) {37 // Redirect to sign in38 window.location.href = "/auth/signin";39 return;40 }4142 if (!plan.priceId) {43 // Free plan - redirect to dashboard44 window.location.href = "/account";45 return;46 }4748 setIsLoading(true);49 try {50 const result = await createCheckoutSession(plan.priceId);5152 if (result.error) {53 console.error("Checkout error:", result.error);54 return;55 }5657 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 };6667 return (68 <Card className={`relative overflow-hidden ${69 plan.popular70 ? "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 Popular77 </div>78 </div>79 )}8081 <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>103104 <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>114115 {/* 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 )}129130 {/* Action Button */}131 <Button132 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.buttonText147 )}148 </Button>149150 {/* 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
Modify subscription plans in the centralized configuration:
1// src/lib/subscriptions.ts2import { SubscriptionType } from "@prisma/client";34export 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: 100020 }21 },22 MONTHLY: {23 name: "Pro Monthly",24 description: "For growing businesses",25 priceValue: 2900, // $29.00 in cents26 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, // unlimited37 storage: "100GB",38 apiCalls: -139 }40 },41 YEARLY: {42 name: "Pro Yearly",43 description: "Best value for teams",44 priceValue: 29000, // $290.00 in cents45 currency: "USD",46 interval: "year",47 stripeProductId: process.env.STRIPE_PRICE_YEARLY,48 savings: {49 amount: 5800, // $58 savings50 percentage: 1751 },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: -162 }63 }64} as const;6566// Helper function to get plan by type67export function getPlan(type: SubscriptionType) {68 return SUBSCRIPTION_PLANS[type];69}7071// Helper to format prices72export function formatPrice(priceInCents: number, currency = "USD") {73 return new Intl.NumberFormat("en-US", {74 style: "currency",75 currency,76 }).format(priceInCents / 100);77}
Customize the appearance and messaging:
1// Custom pricing section variants2export 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>1213 {/* 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}2122// Pricing toggle for monthly/yearly23export function PricingToggle() {24 const [isYearly, setIsYearly] = useState(false);2526 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 Monthly30 </span>31 <button32 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 <span38 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 Yearly45 <Badge variant="secondary" className="ml-2 text-xs">46 Save 17%47 </Badge>48 </span>49 </div>50 );51}5253// Pricing FAQ section54export 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 ];6970 return (71 <div className="mt-24">72 <h3 className="text-2xl font-bold text-center mb-12">73 Frequently Asked Questions74 </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
Handle Stripe checkout sessions securely:
1// src/server/actions/stripe.ts2"use server";34import { auth } from "~/auth";5import { stripe } from "~/server/lib/stripe";6import { db } from "~/server/db";7import { env } from "~/env";89export async function createCheckoutSession(priceId: string) {10 const session = await auth();1112 if (!session?.user?.email) {13 return { error: "Not authenticated" };14 }1516 try {17 // Get or create Stripe customer18 let customer = await stripe.customers.list({19 email: session.user.email,20 limit: 121 });2223 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.id30 }31 });32 customerId = newCustomer.id;3334 // Update user with Stripe customer ID35 await db.user.update({36 where: { id: session.user.id },37 data: { customerId }38 });39 } else {40 customerId = customer.data[0]!.id;41 }4243 // Create Stripe Checkout session44 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 trial58 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 });6869 return { url: checkoutSession.url };70 } catch (error) {71 console.error("Stripe checkout error:", error);72 return { error: "Failed to create checkout session" };73 }74}
Add the pricing section to your pages:
1// src/app/pricing/page.tsx2import { PricingSection } from "~/components/PricingSection";3import { PricingFAQ } from "~/components/PricingFAQ";45export default function PricingPage() {6 return (7 <main className="min-h-screen">8 <PricingSection />9 <PricingFAQ />10 </main>11 );12}1314// src/app/page.tsx (Landing page)15import { PricingSection } from "~/components/PricingSection";1617export 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>2526 {/* Features */}27 <section className="py-24">28 {/* Features content */}29 </section>3031 {/* Pricing */}32 <PricingSection />3334 {/* CTA */}35 <section className="py-24">36 {/* Final CTA */}37 </section>38 </main>39 );40}
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
Stripe Test Mode
Use test card numbers: 4242 4242 4242 4242 (Visa), 4000 0025 0000 3155 (requires authentication)
Conversion Testing
1// Track pricing page conversions2import { trackEvent } from "~/lib/analytics";34// When user views pricing5trackEvent("pricing_viewed", {6 source: "landing_page",7 user_id: session?.user?.id8});910// When user clicks subscribe11trackEvent("subscribe_clicked", {12 plan: planId,13 price: planPrice,14 user_id: session?.user?.id15});1617// When checkout completes18trackEvent("subscription_created", {19 plan: planId,20 revenue: planPrice,21 user_id: session.user.id22});
Pricing Component Ready!
Your pricing section is configured for maximum conversions. Next, learn about theme toggle functionality.