DEV Community

Cover image for How To: Interactive Pricing Component with Tailwind CSS
mematthew123
mematthew123

Posted on

How To: Interactive Pricing Component with Tailwind CSS

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

Component Screenshot
Want to see a live demo? Live Demo
Full Code and Repo Full Code Example

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;
Enter fullscreen mode Exit fullscreen mode

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[];
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode
  • 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">
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode
  • 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> )}
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode
  • 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. We hope you enjoyed this article and if you found this article helpful, give it like, and follow for more guides and tutorials.

Top comments (0)