How To: Interactive Pricing Component with Tailwind CSS

by Matthew Rhoads,

What are Pricing Toggles?

Pricing toggles are interactive elements that allow users to switch between different billing cycles (typically monthly and annual). When implemented well, they provide clear price comparisons and enhance the user experience of pricing pages.

Benefits of Pricing Toggles Pricing toggles offer several advantages for your website:

  • They simplify complex pricing information into an easily digestible format
  • The interactive toggle encourages user engagement
  • They effectively highlight potential savings on annual plans
  • They help users make informed decisions about pricing options
  • They reduce cognitive load by showing relevant pricing based on user preference

What we are building

Component Screenshot

Want to see a live demo? Live Demo Full Code and Repo [Full Code Example] (https://github.com/mematthew123/pricing-demo)

In this guide, we'll cover:

  • How to create a smooth animated toggle switch
  • Using Tailwind's utility classes for clean, responsive design
  • Managing pricing state and calculations
  • Creating consistent pricing cards with feature comparisons
  • Adding hover animations and transitions

Let's dive in and see how it's done!

Initial Setup

If we haven't already let's go ahead and spin up a new Next.js site site using the command npx create-next-app@latest and selecting Tailwind CSS from the setup instructions.

Next we will install the HeroIcons package npm i @heroicons/react We will then need to create a new folder called Components. Inside our new folder we will create a new file called PricingToggle.tsx.

1. Basic Component setup We will want to start by creating a function and passing that to our page.tsx file. Your component should look similar to below:

// PricingToggle.tsx

const PricingToggle = () => {
return (
<div className="max-w-6xl mx-auto p-6">
<div className="flex flex-col items-center mb-12">
<h1 className="text-3xl font-bold text-center">Pricing Component</h1>
</div>
</div>
);
};

export default PricingToggle;

2. Setting Up Types and Data Structure

Next let's start by defining our TypeScript types and organizing our pricing data structure:

type SupportLevel = 'basic' | 'priority'

interface Plan {
  name: string
  monthlyPrice: number
  annualPrice: number
  users: string
  storage: string
  support: SupportLevel
  availableFeatures: string[]
}

This gives us a solid foundation for type safety throughout our component.

3. Defining the Plan Data

Let's add our plan data to the component we've created:

import React, { useState } from 'react';

const PricingToggle = () => {
  const [isAnnual, setIsAnnual] = useState(false);

  const baseFeatures = {
    support: {
      basic: 'Basic Support',
      priority: 'Priority Support'
    } as const
  };

  const additionalFeatures = [
    'Email Reports',
    'Advanced Analytics',
    'Custom Domains',
    'API Access',
    'SSO Authentication'
  ];

  const plans: Plan[] = [
    {
      name: 'Basic',
      monthlyPrice: 9,
      annualPrice: 90,
      users: '1 User',
      storage: '10GB Storage',
      support: 'basic',
      availableFeatures: [
        'Email Reports'
      ]
    },
    {
      name: 'Pro',
      monthlyPrice: 29,
      annualPrice: 290,
      users: '5 Users',
      storage: '100GB Storage',
      support: 'priority',
      availableFeatures: [
        'Email Reports',
        'Advanced Analytics',
        'Custom Domains'
      ]
    },
    {
      name: 'Enterprise',
      monthlyPrice: 99,
      annualPrice: 990,
      users: 'Unlimited Users',
      storage: '1TB Storage',
      support: 'priority',
      availableFeatures: [
        'Email Reports',
        'Advanced Analytics',
        'Custom Domains',
        'API Access',
        'SSO Authentication'
      ]
    }
  ];

  return (
    <div className="max-w-6xl mx-auto p-6">
      {/* Content will go here */}
    </div>
  );
};

export default PricingToggle;

Data Structure Explanation

  • additionalFeatures: Array of all possible features for comparison across plans
  • plans: Array of Plan objects that follow our TypeScript interface
  • Each plan includes pricing for both billing periods, enabling easy switching
  • Features are stored as arrays for flexible rendering and comparison

4. Building the Toggle Switch

Now let's add the toggle switch to our component:

import React, { useState } from 'react';

const PricingToggle = () => {
  const [isAnnual, setIsAnnual] = useState(false);

  const baseFeatures = {
    support: {
      basic: 'Basic Support',
      priority: 'Priority Support'
    } as const
  };

  const additionalFeatures = [
    'Email Reports',
    'Advanced Analytics',
    'Custom Domains',
    'API Access',
    'SSO Authentication'
  ];

  const plans: Plan[] = [
    // ... plans array from previous section
  ];

  return (
    <div className="max-w-6xl mx-auto p-6">
      <div className="flex flex-col items-center mb-12">
        <h2 className="text-3xl font-bold mb-8">Choose Your Plan</h2>
        <div className="relative flex items-center space-x-3">
          <span className={`text-sm transition-colors duration-200 ${
            !isAnnual ? 'text-blue-600 font-bold' : 'text-gray-600'
          }`}>
            Monthly
          </span>
          <button
            onClick={() => setIsAnnual(!isAnnual)}
            className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-300"
            style={{ backgroundColor: isAnnual ? '#3B82F6' : '#E5E7EB' }}
            role="switch"
            aria-checked={isAnnual}
          >
            <span
              className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-lg ring-0 transition duration-300 ease-in-out ${
                isAnnual ? 'translate-x-6' : 'translate-x-1'
              }`}
            />
          </button>
          <span className={`text-sm transition-colors duration-200 ${
            isAnnual ? 'text-blue-600 font-bold' : 'text-gray-600'
          }`}>
            Annual <span className="ml-1 text-green-500 text-xs">Save 20%</span>
          </span>
        </div>
      </div>
    </div>
  );
};

export default PricingToggle;

Tailwind Classes Breakdown

Layout Classes:

  • flex flex-col: Creates a vertical flex container
  • items-center: Centers children horizontally
  • mb-12: Adds margin bottom of 3rem
  • space-x-3: Adds horizontal spacing between flex items

Toggle Button Classes:

  • h-6 w-11: Sets height (1.5rem) and width (2.75rem)
  • rounded-full: Creates fully rounded corners
  • transition-colors duration-300: Smooth color transition over 300ms
  • translate-x-6/translate-x-1: Moves toggle knob between positions

5. Adding Feature List Helpers

Let's add our helper functions and start building out the feature display:

import React, { useState } from 'react';

const PricingToggle = () => {
  const [isAnnual, setIsAnnual] = useState(false);

  // Previous constants remain the same...

  const getUnavailableFeatures = (plan: Plan) => {
    return additionalFeatures.filter(feature => !plan.availableFeatures.includes(feature));
  };

  const renderFeatureItem = (feature: string, isAvailable: boolean) => (
    <li key={feature} className="flex items-center">
      <svg
        className={`h-4 w-4 mr-3 flex-shrink-0 ${
          isAvailable ? 'text-blue-500' : 'text-gray-300'
        }`}
        fill="none"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth="2"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path d="M5 13l4 4L19 7" />
      </svg>
      <span className={isAvailable ? 'text-gray-600' : 'text-gray-300'}>
        {feature}
      </span>
    </li>
  );

  return (
    <div className="max-w-6xl mx-auto p-6">
      <div className="flex flex-col items-center mb-12">
        {/* Toggle switch from previous section */}
      </div>
      {/* We'll add the pricing cards next */}
    </div>
  );
};

export default PricingToggle;

6. Building the Complete Component

Now let's put everything together with the pricing cards grid:

import React, { useState } from 'react';

type SupportLevel = 'basic' | 'priority';

interface Plan {
  name: string;
  monthlyPrice: number;
  annualPrice: number;
  users: string;
  storage: string;
  support: SupportLevel;
  availableFeatures: string[];
}

const PricingToggle = () => {
  const [isAnnual, setIsAnnual] = useState(false);

  const baseFeatures = {
    support: {
      basic: 'Basic Support',
      priority: 'Priority Support'
    } as const
  };

  const additionalFeatures = [
    'Email Reports',
    'Advanced Analytics',
    'Custom Domains',
    'API Access',
    'SSO Authentication'
  ];

  const plans: Plan[] = [
    {
      name: 'Basic',
      monthlyPrice: 9,
      annualPrice: 90,
      users: '1 User',
      storage: '10GB Storage',
      support: 'basic',
      availableFeatures: [
        'Email Reports'
      ]
    },
    {
      name: 'Pro',
      monthlyPrice: 29,
      annualPrice: 290,
      users: '5 Users',
      storage: '100GB Storage',
      support: 'priority',
      availableFeatures: [
        'Email Reports',
        'Advanced Analytics',
        'Custom Domains'
      ]
    },
    {
      name: 'Enterprise',
      monthlyPrice: 99,
      annualPrice: 990,
      users: 'Unlimited Users',
      storage: '1TB Storage',
      support: 'priority',
      availableFeatures: [
        'Email Reports',
        'Advanced Analytics',
        'Custom Domains',
        'API Access',
        'SSO Authentication'
      ]
    }
  ];

  const getUnavailableFeatures = (plan: Plan) => {
    return additionalFeatures.filter(feature => !plan.availableFeatures.includes(feature));
  };

  const renderFeatureItem = (feature: string, isAvailable: boolean) => (
    <li key={feature} className="flex items-center">
      <svg
        className={`h-4 w-4 mr-3 flex-shrink-0 ${isAvailable ? 'text-blue-500' : 'text-gray-300'}`}
        fill="none"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth="2"
        viewBox="0 0 24 24"
        stroke="currentColor"
      >
        <path d="M5 13l4 4L19 7" />
      </svg>
      <span className={isAvailable ? 'text-gray-600' : 'text-gray-300'}>
        {feature}
      </span>
    </li>
  );

  return (
    <div className="max-w-6xl mx-auto p-6">
      <div className="flex flex-col items-center mb-12">
        <h2 className="text-3xl font-bold mb-8">Choose Your Plan</h2>
        <div className="relative flex items-center space-x-3">
          <span className={`text-sm transition-colors duration-200 ${!isAnnual ? 'text-blue-600 font-bold' : 'text-gray-600'}`}>Monthly</span>
          <button
            onClick={() => setIsAnnual(!isAnnual)}
            className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-300"
            style={{ backgroundColor: isAnnual ? '#3B82F6' : '#E5E7EB' }}
            role="switch"
            aria-checked={isAnnual}
          >
            <span
              className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-lg ring-0 transition duration-300 ease-in-out ${
                isAnnual ? 'translate-x-6' : 'translate-x-1'
              }`}
            />
          </button>
          <span className={`text-sm transition-colors duration-200 ${isAnnual ? 'text-blue-600 font-bold' : 'text-gray-600'}`}>
            Annual <span className="ml-1 text-green-500 text-xs">Save 20%</span>
          </span>
        </div>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
        {plans.map((plan) => (
          <div
            key={plan.name}
            className="relative p-6 bg-white rounded-xl border border-gray-200 shadow-lg transition-all duration-300 hover:scale-105 hover:shadow-xl"
          >
            <div className="space-y-4">
              <h3 className="text-xl font-bold text-gray-900">{plan.name}</h3>

              <div className="flex items-baseline text-gray-900">
                <span className="text-2xl font-semibold">$</span>
                <span className="text-4xl font-bold tracking-tight transition-all duration-300">
                  {isAnnual ? Math.round(plan.annualPrice / 12) : plan.monthlyPrice}
                </span>
                <span className="ml-1 text-sm font-medium text-gray-500">/month</span>
              </div>

              {isAnnual && (
                <p className="text-sm text-green-500">
                  ${plan.annualPrice} billed annually
                </p>
              )}

              <ul className="space-y-3 text-sm">
                {/* Core features */}
                {renderFeatureItem(plan.users, true)}
                {renderFeatureItem(plan.storage, true)}
                {renderFeatureItem(baseFeatures.support[plan.support], true)}

                {/* Available features */}
                {plan.availableFeatures.map(feature => renderFeatureItem(feature, true))}

                {/* Divider if there are unavailable features */}
                {getUnavailableFeatures(plan).length > 0 && (
                  <li className="border-t border-gray-200 my-4"></li>
                )}

                {/* Unavailable features */}
                {getUnavailableFeatures(plan).map(feature => renderFeatureItem(feature, false))}
              </ul>

              <button className="mt-6 w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200">
                Get Started
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default PricingToggle;

Understanding the Complete Implementation

Let's break down the key aspects of our final pricing component:

Grid Layout and Responsive Design

<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
  • Starts as a single column on mobile devices
  • Expands to three columns on medium screens (768px and up)
  • Maintains consistent 2rem (32px) gap between cards

Pricing Card Structure

Each card is built with several layers:

<div className="relative p-6 bg-white rounded-xl border border-gray-200 shadow-lg transition-all duration-300 hover:scale-105 hover:shadow-xl">
  • relative positioning for potential overlays or badges
  • p-6 padding creates comfortable spacing
  • Subtle shadow and border for depth
  • Smooth hover animation with scale and enhanced shadow

Price Display

<div className="flex items-baseline text-gray-900">   <span className="text-2xl font-semibold">$</span>  <span className="text-4xl font-bold tracking-tight transition-all duration-300">    {isAnnual ? Math.round(plan.annualPrice / 12) : plan.monthlyPrice}  </span>  <span className="ml-1 text-sm font-medium text-gray-500">/month</span> </div>
  • Aligned at baseline for clean typography
  • Larger price with bold weight for emphasis
  • Smooth transition when switching between billing periods
  • Clear indication of billing frequency

Feature Organization

Features are organized into three distinct sections:

  1. Core features (users, storage, support)
  2. Available additional features
  3. Unavailable features with reduced opacity

The divider appears only when there are unavailable features:

{getUnavailableFeatures(plan).length > 0 && (   <li className="border-t border-gray-200 my-4"></li> )}

Call-to-Action Button

<button className="mt-6 w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200">
  • Full width for maximum visibility
  • Clear hover and focus states for accessibility
  • Smooth color transition on hover
  • Focus ring for keyboard navigation

Key Takeaways

  1. State Management: Single isAnnual state controls all pricing calculations and display toggles
  2. Type Safety: TypeScript interfaces ensure consistent data structure
  3. Responsive Design: Mobile-first approach with breakpoints for larger screens
  4. User Experience:
    • Smooth transitions for all interactive elements
    • Clear visual hierarchy
    • Consistent spacing and alignment
    • Accessible interactive elements

Zephyr Pixels builds websites and online experiences using Sanity and Next.js. Interested in learning more? Get in touch!

More articles

3D Flip card tutorial using Tailwind CSS

Learn how to create 3D flip cards and animations using Next.js and Tailwind CSS

Read more

Missoula-Based Web Development and design

Learn more about working with a Missoula based web development and design agency

Read more

Tell us about your project

Our offices

  • Missoula
    225 W Front St,
    Missoula, MT 59802
  • Helena
    317 Cruse Ave Suite 202,
    Helena, MT 59601
  • Bozeman
    544 E Main St Unit B,
    Bozeman, MT 59715