energy-systems-and-sustainability
Creating a Dynamic Pricing Table with Javascript and Css Grid
Table of Contents
Introduction: Why a Dynamic Pricing Table Matters
Pricing tables are a cornerstone of any product or service website. A static, one-size-fits-all table may present your price points, but a dynamic pricing table — one that updates in real time based on user interactions — can significantly boost conversion rates. By allowing visitors to toggle between monthly and annual billing, see the total cost with add‑ons, or compare features interactively, you create a transparent and engaging experience. In this guide, we’ll build a production‑ready dynamic pricing table using plain JavaScript and CSS Grid. No frameworks, no libraries — just solid, maintainable code that works across modern browsers.
Setting Up the HTML Structure for a Pricing Grid
The foundation of any dynamic pricing table is a clean, semantic HTML structure. We’ll start with a container that will become our CSS Grid, then nest individual pricing cards inside it. Each card should hold the plan name, price, a list of features, and a call‑to‑action button. To enable dynamic updates, we’ll include a data-plan attribute on each card and structure the price container with a class we can target with JavaScript.
<div class="pricing-grid" role="group" aria-label="Pricing plans">
<div class="pricing-card" data-plan="basic">
<h3 class="plan-name">Basic</h3>
<p class="price">
<span class="amount">$10</span>
<span class="period">/mo</span>
</p>
<ul class="features">
<li>5 projects</li>
<li>10 GB storage</li>
<li>Basic support</li>
</ul>
<a href="#" class="btn">Get Started</a>
</div>
<div class="pricing-card featured" data-plan="pro">
<h3 class="plan-name">Pro</h3>
<p class="price">
<span class="amount">$25</span>
<span class="period">/mo</span>
</p>
<ul class="features">
<li>Unlimited projects</li>
<li>100 GB storage</li>
<li>Priority support</li>
</ul>
<a href="#" class="btn">Get Started</a>
</div>
<div class="pricing-card" data-plan="enterprise">
<h3 class="plan-name">Enterprise</h3>
<p class="price">
<span class="amount">$50</span>
<span class="period">/mo</span>
</p>
<ul class="features">
<li>Unlimited projects</li>
<li>1 TB storage</li>
<li>24/7 support</li>
</ul>
<a href="#" class="btn">Contact Sales</a>
</div>
</div>
Note the role="group" and aria-label to improve accessibility. The featured class on the Pro card lets us highlight the most popular option without extra JavaScript.
Building the CSS Grid Layout
CSS Grid makes it straightforward to create a responsive grid that adjusts the number of columns based on the viewport. We define the container as a grid, set a flexible number of columns with auto-fit and minmax(), and add spacing with gap. Inside each card, we use CSS Grid internally to align the price, features, and button.
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.pricing-card {
display: grid;
grid-template-rows: auto auto 1fr auto;
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 2rem;
text-align: center;
background: #fff;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.pricing-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.pricing-card.featured {
border: 2px solid #007bff;
position: relative;
}
.pricing-card.featured::before {
content: "Most Popular";
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: #007bff;
color: #fff;
padding: 4px 16px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
.plan-name {
font-size: 1.5rem;
margin: 0 0 0.5rem;
}
.price {
margin: 0 0 1.5rem;
}
.amount {
font-size: 2.5rem;
font-weight: 700;
}
.period {
font-size: 1rem;
color: #666;
}
.features {
list-style: none;
padding: 0;
margin: 0 0 1.5rem;
}
.features li {
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
.features li:last-child {
border-bottom: none;
}
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
background: #007bff;
color: #fff;
text-decoration: none;
border-radius: 8px;
transition: background 0.3s;
}
.btn:hover {
background: #0056b3;
}
The grid-template-rows ensures that the button always stays at the bottom, even if feature lists differ in length. The featured card’s “Most Popular” badge is created with a pseudoelement to avoid extra markup.
Adding the JavaScript Interactivity
Now comes the dynamic part. We’ll add a toggle switch that lets visitors switch between monthly and yearly pricing. When the toggle changes, JavaScript updates the prices for all cards. We also include a helper to format prices and a smooth transition for the numeric change.
<div class="pricing-toggle">
<label class="toggle-label">
<span>Monthly</span>
<input type="checkbox" id="billing-toggle" role="switch" aria-checked="false">
<span class="toggle-slider"></span>
<span>Yearly (save up to 20%)</span>
</label>
</div>
CSS for the toggle:
.pricing-toggle {
text-align: center;
margin-bottom: 2rem;
}
.toggle-label {
display: inline-flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
user-select: none;
}
.toggle-label input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
width: 50px;
height: 26px;
background: #ccc;
border-radius: 13px;
position: relative;
transition: background 0.3s;
}
.toggle-slider::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform 0.3s;
}
input:checked + .toggle-slider {
background: #007bff;
}
input:checked + .toggle-slider::after {
transform: translateX(24px);
}
JavaScript that powers the update:
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.getElementById('billing-toggle');
const prices = {
basic: { monthly: 10, yearly: 8 }, // per month when billed yearly
pro: { monthly: 25, yearly: 20 },
enterprise: { monthly: 50, yearly: 40 }
};
function updatePrices(isYearly) {
document.querySelectorAll('.pricing-card').forEach(card => {
const plan = card.dataset.plan;
if (!plan || !prices[plan]) return;
const amountElement = card.querySelector('.amount');
const periodElement = card.querySelector('.period');
let amount, period;
if (isYearly) {
amount = prices[plan].yearly;
period = ' /mo (billed yearly)';
} else {
amount = prices[plan].monthly;
period = ' /mo';
}
amountElement.textContent = `$${amount}`;
periodElement.textContent = period;
});
}
toggle.addEventListener('change', () => {
const isYearly = toggle.checked;
updatePrices(isYearly);
toggle.setAttribute('aria-checked', isYearly);
});
// Initialize with monthly
updatePrices(false);
});
The prices object stores both monthly and yearly rates. Yearly prices represent the per-month cost when paying annually — a common pattern. The function updatePrices() iterates over each card, reads its data-plan, and updates the displayed amount and period. We also update the aria-checked attribute to keep the toggle accessible.
Enhancing the Transition
To make the price change feel smooth, we can add a brief CSS transition on the .amount and .period elements. However, since textContent changes instantly, we can simulate a count‑up effect with a small interval, or simply use a fade‑in. For a production site, a quick opacity transition on the price container works well. Add this to your CSS:
.price {
transition: opacity 0.2s ease;
}
.updating .price {
opacity: 0;
}
Then in JavaScript, briefly add and remove the updating class on each card. But for most use cases, the instant update is fine.
Making the Table Responsive and Accessible
CSS Grid already handles responsiveness, but ensure that on very small screens the cards stack in a single column. The minmax(260px, 1fr) value does that automatically. For accessibility, we must ensure the toggle is keyboard‑operable and announces its state. The role="switch" and aria-checked we added already help screen readers. Also, add aria-label to the toggle’s parent label for clarity.
Additionally, provide a visible focus indicator for keyboard users. The default outline works, but you can style it to match your branding:
.toggle-label input:focus-visible + .toggle-slider {
outline: 2px solid #007bff;
outline-offset: 4px;
}
Test the pricing table with a screen reader to verify that price changes are announced. To improve this, after updating the price, you can use a live region:
<div aria-live="polite" aria-atomic="true" class="sr-only"></div>
Then in JavaScript, set its text to something like “Prices updated to yearly billing” after each toggle. This screen‑reader‑only element will announce the change without visual disruption.
Adding More Dynamic Features
Once the core toggle works, you can extend the pricing table with additional interactivity:
Feature Toggles
Add checkboxes for add‑ons (e.g., extra storage, team members) that update the total price. Store the base prices and add‑on costs in an object. When a checkbox changes, recalculate the total for each card accordingly.
Currency Selector
If your audience is global, add a dropdown to switch between USD, EUR, GBP, etc. Store conversion rates and multiply the base price. Update the amount text with proper currency formatting using Intl.NumberFormat.
Animated Price Counter
For a polished effect, animate the number from the old value to the new value over a few hundred milliseconds. Libraries like animate.css are not needed — a simple requestAnimationFrame loop works. However, keep it performant: only animate the visible cards.
Savings Badge
When yearly billing is toggled, show a “Save 20%” badge next to the price. Calculate the savings as ((monthly - yearly) / monthly) * 100 and display it dynamically. Add a data-savings attribute to each card in the HTML and let JavaScript populate it or compute it on the fly.
Performance Considerations
With only a few pricing cards, performance is not a concern. But if you have dozens of plans (e.g., SaaS that generates plans from a database), consider these optimizations:
- Use event delegation: Attach an event listener to a parent container instead of each card. For the toggle, that’s already done. For checkboxes inside cards, delegate.
- Cache DOM references: On initialization, store references to the
.amountand.periodelements in an array or Map by plan key. Then, when updating, you don’t need to query the DOM again. - Use
requestAnimationFramefor animations: Schedule visual updates for the next paint cycle to avoid layout thrashing. - Minimize reflows: Batch DOM changes when updating multiple cards. In our
updatePricesfunction, we read and write to the DOM in a single loop — that’s fine. If you add classes or inline styles, consider reading all values first, then writing all changes.
Real‑World Use Cases and Examples
Dynamic pricing tables are used everywhere from subscription services like Dropbox and Spotify to hosting providers like DigitalOcean. A common pattern is to offer a monthly/yearly toggle with a “Best Value” label on the yearly option. Some companies also show a total annual charge next to the monthly savings. For example, a $10/mo plan with yearly billing might display “$8/mo — billed $96 annually”. Our structure already supports that with the period span.
Another variation is sliding scales for quantity. A pricing table for a SaaS product might have sliders for number of users or storage. As the user moves the slider, the price updates live. CSS Grid handles the layout, and JavaScript updates the price values. This can be built with the same principles: store price tiers, listen to input changes, and update the DOM.
For a more advanced approach, you could load pricing data from a JSON file or API, making the table truly data‑driven. If your site is built with a headless CMS like Directus, the pricing data would come from a collection, and your front‑end JavaScript would fetch it and render the grid dynamically. This approach keeps pricing in sync with your backend.
Testing and Browser Support
CSS Grid is supported in all modern browsers, including Internet Explorer 11 with the -ms- prefix — though we recommend using Autoprefixer or ignoring IE11 for new projects. The JavaScript used here (ES6 const, let, arrow functions, forEach, template literals) works in Chrome, Firefox, Safari, and Edge. If you need to support older browsers, transpile with Babel or use var and traditional functions. The role="switch" is well supported in modern screen readers, but you can also use a standard checkbox with a hidden aria-live region as a fallback.
Alternative Approach: CSS Grid with CSS Custom Properties
An interesting method to avoid JavaScript for toggling prices is to use CSS Custom Properties and :has() (when browser support matures). You would set the prices as --price-basic-monthly: 10; etc., and change them by toggling a class on the body. However, for true dynamic logic (e.g., adding two add‑ons), JavaScript is still required. The combination of both — CSS for layout and transitions, JavaScript for dynamic data — is the most robust.
Conclusion
Building a dynamic pricing table with JavaScript and CSS Grid is both achievable and extends. By starting with semantic HTML, structuring the grid with CSS Grid, and adding straightforward JavaScript for interactivity, you create a tool that can adapt to complex pricing models without bloated dependencies. The same principles — data‑driven updates, responsive layout, accessible controls — scale to enterprise‑level applications. As a next step, consider reading the MDN CSS Grid documentation for more techniques, or explore Sara Soueidan’s accessibility‑first toggle switch design. For handling complex data bindings without a framework, the Vanilla JS Toolkit offers lightweight helpers.
Now implement these patterns in your own project. Your users will appreciate the clarity, and your conversion rates may thank you.