Free Interactive Pricing Table Template | HTML & Tailwind

Faraz

By Faraz -

Get a free, responsive pricing table template. Just copy and paste the HTML, Tailwind CSS, and JavaScript to add a modern, interactive price page to your site.


free-interactive-pricing-table-template-html-and-tailwind.webp

Table of Contents

  1. Project Introduction
  2. HTML Code
  3. CSS Code
  4. JavaScript Code
  5. Conclusion
  6. Preview

A pricing table is one of the most important parts of any business or SaaS website. It helps users easily compare plans and select the one that fits their needs.

In this post, we’ll create a responsive and interactive pricing table using:

  • HTML for the structure
  • Tailwind CSS for quick styling
  • Custom CSS for unique animations and effects
  • JavaScript for interactive elements like toggles and dynamic pricing updates

There are no extra libraries required except Tailwind CSS, which we’ll include using a simple CDN link:

<script src="https://cdn.tailwindcss.com"></script>

Source Code

Step 1 (HTML Code):

This is the main structure of our pricing page. It includes the layout, cards, toggles, and price display area. All the necessary scripts (Tailwind, Google Fonts) are linked in the <head>.

Step 2 (CSS Code):

This CSS works with Tailwind to create the custom styles for the toggle, cards, checkboxes, and slider.

Paste this code into a new file named styles.css.

Then, make sure to link it in the <head> of your HTML file.:

body {
  font-family: 'Inter', sans-serif;
  background-color: #f9fafb; /* gray-50 */
  color: #111827; /* gray-900 */
}

/* --- Custom Toggle Switch Styling --- */
.toggle-knob {
  transition: transform 0.3s ease-in-out;
}
input:checked + .toggle-slider .toggle-knob {
  transform: translateX(24px); /* 6 * 4px = 24px */
}
input:checked + .toggle-slider {
  background-color: #4f46e5; /* indigo-600 */
}

/* --- Plan Selector Card (Left Side) --- */
.plan-selector-card {
  background-color: #ffffff;
  border: 2px solid #e5e7eb; /* gray-200 */
  border-radius: 1rem; /* rounded-2xl */
  padding: 1.5rem;
  cursor: pointer;
  transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
  transform: scale(1);
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05),
    0 2px 4px -1px rgba(0, 0, 0, 0.03); /* Added subtle shadow */
}

.plan-selector-card:hover {
  border-color: #a5b4fc; /* indigo-300 */
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05);
}

.plan-selector-card.active {
  border-color: #4f46e5; /* indigo-600 */
  box-shadow: 0 20px 25px -5px rgba(79, 70, 229, 0.1),
    0 10px 10px -5px rgba(79, 70, 229, 0.04);
  transform: scale(1.03);
  background-color: #f0f5ff; /* A very light indigo */
}

/* --- Custom Add-On Checkbox Styling --- */
.add-on-label {
  display: flex;
  align-items: center;
  padding: 1rem;
  background-color: #f9fafb; /* gray-50 */
  border-radius: 0.75rem; /* rounded-lg */
  border: 2px solid #f3f4f6; /* gray-100 */
  cursor: pointer;
  transition: all 0.2s ease;
}
.add-on-label:hover {
  border-color: #d1d5db; /* gray-300 */
}

.add-on-checkbox {
  -webkit-appearance: none;
  appearance: none;
  height: 1.5rem; /* h-6 */
  width: 1.5rem; /* w-6 */
  flex-shrink: 0;
  background-color: #ffffff;
  border: 2px solid #d1d5db; /* gray-300 */
  border-radius: 0.375rem; /* rounded-md */
  margin-right: 0.75rem; /* mr-3 */
  display: grid;
  place-content: center;
  transition: all 0.2s ease;
}

.add-on-checkbox:checked {
  background-color: #4f46e5; /* indigo-600 */
  border-color: #4f46e5;
}

.add-on-checkbox::before {
  content: '';
  width: 0.8rem;
  height: 0.8rem;
  transform: scale(0);
  transition: 120ms transform ease-in-out;
  box-shadow: inset 1em 1em #ffffff;
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}

.add-on-checkbox:checked::before {
  transform: scale(1);
}

.add-on-label.peer-checked {
  border-color: #4f46e5;
  background-color: #f0f5ff;
}

/* --- Dynamic Feature List (Right Side) --- */
.feature-item {
  display: flex;
  align-items: center;
  padding: 0.75rem 0; /* py-3 */
  border-bottom: 1px solid #f3f4f6; /* gray-100 */
  transition: all 0.3s ease-in-out;
  opacity: 1;
}

.feature-item.disabled {
  opacity: 0.6;
}

.feature-item .feature-name {
  transition: all 0.3s ease;
}

.feature-item.disabled .feature-name {
  text-decoration: line-through;
  color: #6b7280; /* gray-500 */
}

.icon-wrapper {
  width: 1.25rem; /* w-5 */
  height: 1.25rem; /* h-5 */
  margin-right: 0.75rem; /* mr-3 */
  flex-shrink: 0;
  position: relative;
}

.icon-check,
.icon-cross {
  position: absolute;
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.icon-check {
  color: #4f46e5; /* indigo-600 */
  opacity: 1;
  transform: scale(1);
}
.icon-cross {
  color: #9ca3af; /* gray-400 */
  opacity: 0;
  transform: scale(0.5);
}

.feature-item.disabled .icon-check {
  opacity: 0;
  transform: scale(0.5);
}
.feature-item.disabled .icon-cross {
  opacity: 1;
  transform: scale(1);
}

/* --- Animated Price Counter --- */
#plan-price-value {
  transition: all 0.3s ease;
}

/* --- Custom Range Slider --- */
.price-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 8px;
  background: #e5e7eb; /* gray-200 */
  border-radius: 9999px;
  outline: none;
  transition: opacity 0.2s;
}

/* Thumb (Chrome, Safari, etc.) */
.price-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 24px;
  height: 24px;
  background: #4f46e5; /* indigo-600 */
  border-radius: 50%;
  cursor: pointer;
  border: 4px solid #ffffff;
  box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2);
  transition: all 0.2s ease;
}
.price-slider:hover::-webkit-slider-thumb {
  transform: scale(1.1);
}

/* Thumb (Firefox) */
.price-slider::-moz-range-thumb {
  width: 24px;
  height: 24px;
  background: #4f46e5;
  border-radius: 50%;
  cursor: pointer;
  border: 4px solid #ffffff;
  box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2);
  transition: all 0.2s ease;
}
.price-slider:hover::-moz-range-thumb {
  transform: scale(1.1);
}
 

Step 3 (JavaScript Code):

<p>This is the "brains" of the operation, rewritten to use per-user pricing and to default to the "Pro" plan. Place this in the <code>&lt;script&gt;</code> tag at the bottom of your <code>&lt;body&gt;</code>.</p>
document.addEventListener('DOMContentLoaded', () => {
  // --- DATA ---
  const plansData = {
    starter: {
      name: 'Starter',
      description: 'Ideal for individuals getting started and hobby projects.',
      monthlyPricePerUser: 29,
      annualPricePerUser: 23,
      minUsers: 1,
      maxUsers: 10,
      defaultUsers: 5,
      ctaText: 'Get Started with Starter',
    },
    pro: {
      name: 'Pro',
      description:
        'For growing teams and professionals who need advanced tools.',
      monthlyPricePerUser: 79,
      annualPricePerUser: 63,
      minUsers: 5,
      maxUsers: 50,
      defaultUsers: 10,
      ctaText: 'Choose Pro',
    },
    enterprise: {
      name: 'Enterprise',
      description:
        'Tailored solutions for large organizations and complex needs.',
      monthlyPrice: 'Custom',
      annualPrice: 'Custom',
      minUsers: 50,
      maxUsers: 500,
      defaultUsers: 50,
      ctaText: 'Contact Sales',
    },
  };

  const addOnsData = {
    ai: {
      monthlyPrice: 10,
      annualPrice: 8,
    },
    support: {
      monthlyPrice: 15,
      annualPrice: 12,
    },
  };

  const allFeatures = [
    { id: 'projects', name: 'Up to 10 Projects', plans: ['starter'] },
    {
      id: 'projects-pro',
      name: 'Unlimited Projects',
      plans: ['pro', 'enterprise'],
    },
    { id: 'team-members', name: '5 Team Members', plans: ['starter'] },
    {
      id: 'team-members-pro',
      name: 'Unlimited Team Members',
      plans: ['pro', 'enterprise'],
    },
    { id: 'storage', name: '50GB Storage', plans: ['starter', 'pro'] },
    { id: 'storage-ent', name: 'Unlimited Storage', plans: ['enterprise'] },
    {
      id: 'analytics',
      name: 'Basic Analytics',
      plans: ['starter', 'pro', 'enterprise'],
    },
    { id: 'integrations', name: 'Basic Integrations', plans: ['starter'] },
    {
      id: 'integrations-pro',
      name: 'Advanced Integrations',
      plans: ['pro', 'enterprise'],
    },
    { id: 'ai-tools', name: 'Advanced AI Tools', plans: ['pro', 'enterprise'] },
    { id: 'api-access', name: 'API Access', plans: ['pro', 'enterprise'] },
    {
      id: 'priority-support',
      name: 'Priority Chat Support',
      plans: ['pro', 'enterprise'],
    },
    { id: 'sso', name: 'Advanced SSO & Security', plans: ['enterprise'] },
    { id: 'sla', name: 'Uptime SLA', plans: ['enterprise'] },
    {
      id: 'dedicated-support',
      name: '24/7/356 Dedicated Support',
      plans: ['enterprise'],
    },
  ];

  // --- DOM ELEMENTS ---
  const toggle = document.getElementById('billing-toggle');
  const planSelectors = document.querySelectorAll('.plan-selector-card');
  const selectorPriceSpans = document.querySelectorAll(
    '.plan-selector-card span[data-monthly-price]'
  );

  const planNameEl = document.getElementById('plan-name');
  const planDescriptionEl = document.getElementById('plan-description');
  const planPriceValueEl = document.getElementById('plan-price-value');
  const planPricePeriodEl = document.getElementById('plan-price-period');
  const ctaButton = document.getElementById('cta-button');
  const featureListEl = document.getElementById('feature-list');

  const sliderContainer = document.getElementById('slider-container');
  const teamSlider = document.getElementById('team-slider');
  const sliderValueDisplay = document.getElementById('slider-value-display');

  const addOnsContainer = document.getElementById('add-ons-container');
  const addOnCheckboxes = document.querySelectorAll('.add-on-checkbox');
  const addOnPriceSpans = document.querySelectorAll('.add-on-price');

  let currentPlanId = 'starter';
  let isAnnual = false;
  let priceAnimationInterval = null;
  let activeAddOns = new Set();

  // --- FUNCTIONS ---

  // 1. Initialize: Render all possible features into the list
  function initializeFeatureList() {
    allFeatures.forEach((feature) => {
      const li = document.createElement('li');
      li.className = 'feature-item';
      li.dataset.featureId = feature.id;
      li.innerHTML = `
      <div class="icon-wrapper">
      <svg class="icon-check w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
      <svg class="icon-cross w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
  </div>
  <span class="feature-name text-gray-700">${feature.name}</span>
            `;
      featureListEl.appendChild(li);
    });
  }

  // 2. Animate Price Counter
  function animatePrice(newPrice) {
    if (typeof newPrice !== 'number') {
      if (priceAnimationInterval) clearInterval(priceAnimationInterval);
      planPriceValueEl.innerText = newPrice;
      planPriceValueEl.dataset.currentPrice = 'NaN'; // Mark as non-numeric
      return;
    }

    let currentPrice = parseInt(planPriceValueEl.dataset.currentPrice);
    if (isNaN(currentPrice)) {
      currentPrice = 0;
    }

    clearInterval(priceAnimationInterval); // Clear any existing animation

    const step =
      newPrice > currentPrice
        ? Math.ceil((newPrice - currentPrice) / 10)
        : Math.floor((newPrice - currentPrice) / 10);
    if (step === 0) {
      // Already at target
      planPriceValueEl.dataset.currentPrice = newPrice;
      planPriceValueEl.innerText = newPrice; // Set text if no animation
      return;
    }

    let animatedPrice = currentPrice; // Use a variable for animation state

    priceAnimationInterval = setInterval(() => {
      if (animatedPrice !== newPrice) {
        animatedPrice += step;
        // Overshoot/undershoot check
        if (
          (step > 0 && animatedPrice >= newPrice) ||
          (step < 0 && animatedPrice <= newPrice)
        ) {
          animatedPrice = newPrice;
          clearInterval(priceAnimationInterval);
        }
        planPriceValueEl.innerText = animatedPrice; // Update text with animated value
      } else {
        clearInterval(priceAnimationInterval);
      }
    }, 20); // Animation speed
    planPriceValueEl.dataset.currentPrice = newPrice; // Set the new *target* price
  }

  // 3. Update all UI components based on selected plan
  function updatePlanDetails(planId) {
    currentPlanId = planId;
    const planData = plansData[planId];
    isAnnual = toggle.checked;

    // Reset add-ons when changing plans
    activeAddOns.clear();
    addOnCheckboxes.forEach((cb) => (cb.checked = false));

    // Update text details
    planNameEl.innerText = planData.name;
    planDescriptionEl.innerText = planData.description;
    ctaButton.innerText = planData.ctaText;
    ctaButton.href = `#${planId}`;

    // Show/Hide Add-ons & Slider
    if (planId === 'enterprise') {
      addOnsContainer.style.display = 'none';
      sliderContainer.style.display = 'none';
    } else {
      addOnsContainer.style.display = 'block';
      sliderContainer.style.display = 'block';

      // Update slider properties
      teamSlider.min = planData.minUsers;
      teamSlider.max = planData.maxUsers;
      teamSlider.value = planData.defaultUsers;
      updateSliderDisplay();
    }

    // Update price (will be called by updateTotalPrice)
    updateTotalPrice();

    // Update feature list
    allFeatures.forEach((feature) => {
      const featureEl = document.querySelector(
        `.feature-item[data-feature-id="${feature.id}"]`
      );
      if (feature.plans.includes(planId)) {
        featureEl.classList.remove('disabled');
      } else {
        featureEl.classList.add('disabled');
      }
    });

    // Update selector card active state
    planSelectors.forEach((card) => {
      if (card.dataset.planId === planId) {
        card.classList.add('active');
      } else {
        card.classList.remove('active');
      }
    });
  }

  // 4. Update prices on the selector cards (left side)
  function updateSelectorCardPrices() {
    isAnnual = toggle.checked;
    selectorPriceSpans.forEach((span) => {
      const price = isAnnual
        ? span.dataset.annualPrice
        : span.dataset.monthlyPrice;
      span.innerText = `$${price}`;
    });
  }

  // 5. Update Add-on price text
  function updateAddOnPrices() {
    isAnnual = toggle.checked;
    addOnPriceSpans.forEach((span) => {
      const monthly = span.dataset.monthlyPrice;
      const annual = span.dataset.annualPrice;
      span.innerText = isAnnual ? `$${annual}` : `$${monthly}`;
    });
  }

  // 6. Calculate Total Price
  function updateTotalPrice() {
    const planData = plansData[currentPlanId];
    isAnnual = toggle.checked;

    // Handle Enterprise "Custom" price
    if (currentPlanId === 'enterprise') {
      animatePrice('Custom');
      planPricePeriodEl.innerText = 'Custom Pricing';
      return;
    }

    const selectedUsers = parseInt(teamSlider.value);
    const perUserPrice = isAnnual
      ? planData.annualPricePerUser
      : planData.monthlyPricePerUser;
    const basePlanPrice = selectedUsers * perUserPrice;

    let totalAddOnPrice = 0;
    activeAddOns.forEach((addOnId) => {
      const addOnData = addOnsData[addOnId];
      // Making add-ons a flat monthly fee, not per-user
      totalAddOnPrice += isAnnual
        ? addOnData.annualPrice
        : addOnData.monthlyPrice;
    });

    const totalPrice = basePlanPrice + totalAddOnPrice;
    animatePrice(totalPrice);
    planPricePeriodEl.innerText = isAnnual ? 'per year' : 'per month';
  }

  // 7. Update Slider Value Display
  function updateSliderDisplay() {
    const users = teamSlider.value;
    sliderValueDisplay.innerText = `${users} Member${users > 1 ? 's' : ''}`;
  }

  // --- EVENT LISTENERS ---
  // Billing toggle
  toggle.addEventListener('change', () => {
    isAnnual = toggle.checked;
    updateSelectorCardPrices();
    updateAddOnPrices();
    updateTotalPrice(); // Re-calculate total price
  });

  // Team slider
  teamSlider.addEventListener('input', () => {
    updateSliderDisplay();
    updateTotalPrice();
  });

  // Plan selector cards
  planSelectors.forEach((card) => {
    card.addEventListener('click', () => {
      const planId = card.dataset.planId;
      updatePlanDetails(planId);
    });
  });

  // Add-on checkboxes
  addOnCheckboxes.forEach((checkbox) => {
    checkbox.addEventListener('change', () => {
      const addOnId = checkbox.dataset.addOnId;
      if (checkbox.checked) {
        activeAddOns.add(addOnId);
      } else {
        activeAddOns.delete(addOnId);
      }
      updateTotalPrice(); // Recalculate price on add-on change
    });
  });

  // --- INITIALIZATION ---
  initializeFeatureList();
  updateSelectorCardPrices();
  updateAddOnPrices();
  // Set "Pro" as the default active plan on load
  const defaultPlanId = 'pro';
  currentPlanId = defaultPlanId;
  document
    .querySelector(`.plan-selector-card[data-plan-id="${defaultPlanId}"]`)
    .classList.add('active');
  updatePlanDetails(defaultPlanId);

  // Set initial price without animation
  const initialPlan = plansData[defaultPlanId];
  const initialUsers = initialPlan.defaultUsers;
  const initialPrice = initialPlan.monthlyPricePerUser * initialUsers;
  planPriceValueEl.innerText = initialPrice;
  planPriceValueEl.dataset.currentPrice = initialPrice;
  teamSlider.value = initialUsers;
  updateSliderDisplay();
});

Final Output:

free-interactive-pricing-table-template-html-and-tailwind.gif

Conclusion:

And that's it! You now have a fully functional, responsive, and interactive pricing table that uses a per-user pricing model. Users can see exactly what they're getting and how much it will cost, all updated in real-time.

From here, you can easily customize this code. Try changing the plan data in the plansData JavaScript object to match your own products, or adjust the Tailwind CSS utility classes in the HTML to change the colors and spacing to match your brand.

That’s a wrap!

I hope you enjoyed this post. Now, with these examples, you can create your own amazing page.

Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee

And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

Thanks!
Faraz 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Components

Please allow ads on our site🥺