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.
Bootstrap class
No dedicated Bootstrap class
Common 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 and classes
| Variant | Description and classes |
|---|---|
| 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.btn-group rounded-pill btn btn-primary btn-outline-secondary |
| 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.form-check form-switch form-check-inline |
| 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.btn-group badge bg-success rounded-pill d-inline-flex 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.row g-4 col-md-4 card card-body |
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 template variants
Canvas Builder builds pages on the Canvas HTML template, which adds its own styled versions of Bootstrap 5 components. Variants you can ask for in a prompt:
- 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.