Bootstrap 5 Pricing Toggle
A Pricing Toggle is a UI control that switches a pricing section between two billing periods — typically monthly and annual — revealing different price figures without a page reload. It is composed from Bootstrap 5 primitives including btn-group or form-check for the switch, collapse or data-attribute toggling for content visibility, and card or list-group for the pricing tiers. Use it on any SaaS, subscription, or membership pricing page where showing both billing options side by side would add visual clutter.
Primary Class
btn-group / form-check form-switchCommon Use Cases
- →A SaaS product page where users switch between monthly and annual subscription prices to see the discounted yearly rate before choosing a plan.
- →A gym membership site that lets visitors toggle between pay-as-you-go monthly pricing and a discounted 12-month commitment to encourage annual sign-ups.
- →A cloud hosting provider displaying monthly on-demand pricing versus reserved-instance annual pricing for each server tier so engineers can compare total cost of ownership.
- →A project management tool onboarding flow where new users choose their billing cycle before entering payment details, with prices updating immediately on toggle.
Variants & Classes
| Variant | Description |
|---|---|
| Button Group Toggle | Two pill-shaped buttons (Monthly / Annual) styled with btn-group. The active button uses btn-primary; the inactive uses btn-outline-secondary. JavaScript swaps a .d-none class on the relevant price elements. |
| Form Switch Toggle | A single Bootstrap form-check form-switch flanked by 'Monthly' and 'Annual' labels. Checking the switch hides monthly prices and shows annual prices via JavaScript toggling .d-none. Accessible via role and aria-checked. |
| Toggle with Savings Badge | Extends the button group variant by appending a badge next to the 'Annual' label showing the percentage saving (e.g. 'Save 20%'). Uses badge bg-success rounded-pill positioned with d-inline-flex align-items-center gap-2. |
| Card Grid Pricing with Toggle | Three Bootstrap cards in a row (col-md-4) each containing two price spans — one for monthly, one for annual — with .price-monthly and .price-annual classes. The toggle adds d-none to the inactive set across all cards simultaneously. |
Code Example
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pricing Toggle</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.pricing-card { transition: transform .2s; }
.pricing-card:hover { transform: translateY(-4px); }
.price-display { font-size: 2.5rem; font-weight: 700; line-height: 1; }
.price-period { font-size: .875rem; color: #6c757d; }
</style>
</head>
<body class="bg-light py-5">
<div class="container">
<!-- Toggle control -->
<div class="d-flex justify-content-center align-items-center gap-3 mb-5">
<span class="fw-semibold" id="label-monthly">Monthly</span>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch"
id="billingToggle" style="width:3rem;height:1.5rem;cursor:pointer;"
aria-label="Toggle annual billing">
</div>
<span class="fw-semibold" id="label-annual">
Annual
<span class="badge bg-success rounded-pill ms-1" style="font-size:.7rem;">Save 20%</span>
</span>
</div>
<!-- Pricing cards -->
<div class="row g-4 justify-content-center">
<!-- Starter -->
<div class="col-12 col-md-4">
<div class="card h-100 shadow-sm pricing-card">
<div class="card-body p-4">
<h5 class="card-title fw-bold">Starter</h5>
<p class="text-muted small">For individuals and small projects.</p>
<div class="my-3">
<span class="price-display price-monthly">£9</span>
<span class="price-display price-annual d-none">£7</span>
<span class="price-period"> / mo</span>
</div>
<p class="text-muted x-small price-annual d-none">Billed as £84 / year</p>
<ul class="list-unstyled mb-4">
<li class="mb-1">✓ 3 projects</li>
<li class="mb-1">✓ 5 GB storage</li>
<li class="mb-1">✓ Email support</li>
</ul>
<a href="#" class="btn btn-outline-primary w-100">Get started</a>
</div>
</div>
</div>
<!-- Pro (highlighted) -->
<div class="col-12 col-md-4">
<div class="card h-100 shadow border-primary pricing-card">
<div class="card-header bg-primary text-white text-center fw-semibold">Most Popular</div>
<div class="card-body p-4">
<h5 class="card-title fw-bold">Pro</h5>
<p class="text-muted small">For growing teams and businesses.</p>
<div class="my-3">
<span class="price-display price-monthly">£29</span>
<span class="price-display price-annual d-none">£23</span>
<span class="price-period"> / mo</span>
</div>
<p class="text-muted small price-annual d-none">Billed as £276 / year</p>
<ul class="list-unstyled mb-4">
<li class="mb-1">✓ Unlimited projects</li>
<li class="mb-1">✓ 50 GB storage</li>
<li class="mb-1">✓ Priority support</li>
</ul>
<a href="#" class="btn btn-primary w-100">Get started</a>
</div>
</div>
</div>
<!-- Enterprise -->
<div class="col-12 col-md-4">
<div class="card h-100 shadow-sm pricing-card">
<div class="card-body p-4">
<h5 class="card-title fw-bold">Enterprise</h5>
<p class="text-muted small">For large organisations with custom needs.</p>
<div class="my-3">
<span class="price-display price-monthly">£79</span>
<span class="price-display price-annual d-none">£63</span>
<span class="price-period"> / mo</span>
</div>
<p class="text-muted small price-annual d-none">Billed as £756 / year</p>
<ul class="list-unstyled mb-4">
<li class="mb-1">✓ Unlimited everything</li>
<li class="mb-1">✓ 500 GB storage</li>
<li class="mb-1">✓ Dedicated account manager</li>
</ul>
<a href="#" class="btn btn-outline-primary w-100">Contact sales</a>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
(function () {
const toggle = document.getElementById('billingToggle');
const monthly = document.querySelectorAll('.price-monthly');
const annual = document.querySelectorAll('.price-annual');
toggle.addEventListener('change', function () {
const isAnnual = this.checked;
monthly.forEach(el => el.classList.toggle('d-none', isAnnual));
annual.forEach(el => el.classList.toggle('d-none', !isAnnual));
});
})();
</script>
</body>
</html>Live Examples
Button Group Toggle
Two buttons in a btn-group act as the billing period selector. JavaScript swaps d-none on .price-monthly and .price-annual spans when a button is clicked.
Compact Inline Switch with Labels
A minimal form-switch flanked by inline text labels. Suitable for embedding inside an existing pricing section without a card grid.
Toggle with Animated Price Transition
Adds a brief CSS opacity fade when prices switch, giving the toggle a polished feel without any additional libraries.
Canvas Framework Variants
The Canvas template extends Bootstrap 5 with 1,658+ component variants. Generate any of these using Canvas Builder:
- ✓Canvas Builder generates a three-column card grid pricing toggle from a prompt such as 'monthly annual pricing switcher with three tiers'
- ✓Ask Canvas Builder for a dark-mode pricing toggle and it outputs the same pattern with bg-dark card and text-light utilities applied throughout
- ✓Canvas Builder can scaffold a pricing toggle with a pre-selected annual tab and a 'Most Popular' badge on the middle card in one prompt
- ✓Prompt Canvas Builder with 'pricing table toggle no JavaScript' to receive a version that uses hidden radio inputs and CSS :checked selectors instead
- ✓Canvas Builder supports a compact single-product pricing toggle variant suitable for embedding inside a hero section rather than a full pricing page
Best Practices
Use data attributes to associate prices with their period
Rather than relying on DOM order, add data-billing="monthly" and data-billing="annual" to each price element. Your toggle script can then query document.querySelectorAll('[data-billing]') and filter by value, making the markup far easier to extend when you add a third tier or a quarterly option later.
Preserve layout height to avoid content shift
When monthly and annual price strings differ in character length (e.g. '£9' vs '£63'), hiding one and showing the other can cause the card to resize and shift surrounding content. Set a min-height on the price container or use CSS visibility: hidden instead of d-none for the inactive price so the space is always reserved.
Ensure keyboard and screen-reader accessibility
The form-switch input is natively focusable and announces its checked state to screen readers, but add an aria-label describing the action (e.g. 'Switch to annual billing') because the flanking text labels are not programmatically associated with the input. If you use a btn-group instead, apply aria-pressed='true'/'false' to each button and update it in your change handler.
Reflect the selected period in the CTA URL
Append a query parameter such as ?billing=annual to each plan's sign-up link when the annual toggle is active. This lets your back-end or landing page pre-select the correct billing option on the checkout page, reducing friction and abandonment.