Patterns
Pricing
Pricing
Pricing section patterns for SaaS and product landing pages. Tiered cards, feature comparison tables, and billing toggle options.
Overview
Pricing sections help users understand and compare your offerings. These patterns use data-layout="grid" for responsive card layouts and semantic table markup for feature comparisons, creating accessible and mobile-friendly pricing displays.
Key features:
Responsive card grids with data-layout="grid"
Highlighted recommended tier
Feature comparison tables with sticky headers
Monthly/yearly billing toggle with :has() selector
Accessible markup with proper semantics
No JavaScript required for basic functionality
Pricing Cards
Three-tier pricing cards in a responsive grid. The middle tier is highlighted as "Most Popular" with accent styling and a badge.
<section class="pricing-section" data-layout="center" data-layout-max="wide" data-layout-gap="2xl">
<header data-layout="stack" data-layout-gap="s">
<h2>Simple, transparent pricing</h2>
<p class="text-muted">Choose the plan that's right for you</p>
</header>
<div data-layout="grid" data-layout-min="280px" data-layout-gap="l">
<article class="pricing-card" data-layout="stack" data-layout-gap="l">
<div data-layout="stack" data-layout-gap="xs">
<span class="plan-name">Basic</span>
<div>
<span class="price">$9</span>
<span class="price-period">/month</span>
</div>
<p class="text-sm text-muted">Perfect for individuals and small projects</p>
</div>
<ul class="feature-list">
<li><icon-wc name="check" size="sm"></icon-wc> Up to 3 projects</li>
<li><icon-wc name="check" size="sm"></icon-wc> 5 GB storage</li>
<li><icon-wc name="check" size="sm"></icon-wc> Basic analytics</li>
<li><icon-wc name="check" size="sm"></icon-wc> Email support</li>
</ul>
<a href="#" class="button secondary full-width">Get Started</a>
</article>
<article class="pricing-card" data-featured data-layout="stack" data-layout-gap="l">
<span class="pricing-badge">Most Popular</span>
<div data-layout="stack" data-layout-gap="xs">
<span class="plan-name">Pro</span>
<div>
<span class="price">$29</span>
<span class="price-period">/month</span>
</div>
<p class="text-sm text-muted">Best for growing teams and businesses</p>
</div>
<ul class="feature-list">
<li><icon-wc name="check" size="sm"></icon-wc> Unlimited projects</li>
<li><icon-wc name="check" size="sm"></icon-wc> 50 GB storage</li>
<li><icon-wc name="check" size="sm"></icon-wc> Advanced analytics</li>
<li><icon-wc name="check" size="sm"></icon-wc> Priority support</li>
<li><icon-wc name="check" size="sm"></icon-wc> Custom integrations</li>
<li><icon-wc name="check" size="sm"></icon-wc> Team collaboration</li>
</ul>
<a href="#" class="button full-width">Get Started</a>
</article>
<article class="pricing-card" data-layout="stack" data-layout-gap="l">
<div data-layout="stack" data-layout-gap="xs">
<span class="plan-name">Enterprise</span>
<div>
<span class="price">$99</span>
<span class="price-period">/month</span>
</div>
<p class="text-sm text-muted">For large organizations with custom needs</p>
</div>
<ul class="feature-list">
<li><icon-wc name="check" size="sm"></icon-wc> Everything in Pro</li>
<li><icon-wc name="check" size="sm"></icon-wc> Unlimited storage</li>
<li><icon-wc name="check" size="sm"></icon-wc> Custom analytics</li>
<li><icon-wc name="check" size="sm"></icon-wc> 24/7 phone support</li>
<li><icon-wc name="check" size="sm"></icon-wc> Dedicated account manager</li>
<li><icon-wc name="check" size="sm"></icon-wc> SLA guarantee</li>
</ul>
<a href="#" class="button secondary full-width">Contact Sales</a>
</article>
</div>
</section>
Required CSS
Add these styles for pricing card layouts:
.pricing-card {
background: var(--color-surface-raised);
border: var(--border-width-thin) solid var(--color-border);
border-radius: var(--radius-l);
padding: var(--size-l);
text-align: center;
}
.pricing-card[data-featured] {
border-color: var(--color-interactive);
box-shadow: var(--shadow-m);
transform: scale(1.05);
position: relative;
}
.pricing-badge {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
background: var(--color-interactive);
color: var(--color-white);
padding: var(--size-2xs) var(--size-s);
border-radius: var(--radius-full);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
}
.pricing-card .plan-name {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-text);
}
.pricing-card .price {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
}
.pricing-card .price-period {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
.feature-list {
list-style: none;
padding: 0;
margin: 0;
text-align: left;
}
.feature-list li {
display: flex;
align-items: center;
gap: var(--size-s);
padding: var(--size-xs) 0;
}
.feature-list icon-wc {
color: var(--color-success);
flex-shrink: 0;
}
Comparison Table
A feature comparison table with plan columns and feature rows. Uses semantic <table> markup with proper scope attributes for accessibility. The header is sticky for easy comparison while scrolling.
<div class="pricing-table-wrapper">
<table class="pricing-table">
<thead>
<tr>
<th scope="col">Features</th>
<th scope="col">
<div class="plan-header">
<span class="plan-name">Basic</span>
<span class="plan-price">$9<span class="plan-period">/mo</span></span>
</div>
</th>
<th scope="col" class="plan-featured">
<div class="plan-header">
<span class="plan-name">Pro</span>
<span class="plan-price">$29<span class="plan-period">/mo</span></span>
</div>
</th>
<th scope="col">
<div class="plan-header">
<span class="plan-name">Enterprise</span>
<span class="plan-price">$99<span class="plan-period">/mo</span></span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr class="category-row">
<td colspan="4">Usage</td>
</tr>
<tr>
<td>Projects</td>
<td>3</td>
<td>Unlimited</td>
<td>Unlimited</td>
</tr>
<tr>
<td>Storage</td>
<td>5 GB</td>
<td>50 GB</td>
<td>Unlimited</td>
</tr>
<tr class="category-row">
<td colspan="4">Features</td>
</tr>
<tr>
<td>Custom integrations</td>
<td><icon-wc name="x" size="sm" class="feature-unavailable"></icon-wc></td>
<td><icon-wc name="check" size="sm" class="feature-check"></icon-wc></td>
<td><icon-wc name="check" size="sm" class="feature-check"></icon-wc></td>
</tr>
<tr>
<td>API access</td>
<td><icon-wc name="x" size="sm" class="feature-unavailable"></icon-wc></td>
<td><icon-wc name="check" size="sm" class="feature-check"></icon-wc></td>
<td><icon-wc name="check" size="sm" class="feature-check"></icon-wc></td>
</tr>
</tbody>
<tfoot>
<tr>
<td></td>
<td><a href="#" class="button secondary">Get Started</a></td>
<td><a href="#" class="button">Get Started</a></td>
<td><a href="#" class="button secondary">Contact Sales</a></td>
</tr>
</tfoot>
</table>
</div>
Required CSS
Add these styles for pricing tables:
.pricing-table-wrapper {
overflow-x: auto;
}
.pricing-table {
width: 100%;
border-collapse: collapse;
background: var(--color-surface-raised);
border-radius: var(--radius-l);
overflow: hidden;
}
.pricing-table thead {
position: sticky;
top: 0;
background: var(--color-surface-raised);
z-index: 1;
}
.pricing-table th,
.pricing-table td {
padding: var(--size-m);
border-bottom: var(--border-width-thin) solid var(--color-border);
}
.pricing-table th {
font-weight: var(--font-weight-semibold);
text-align: center;
vertical-align: bottom;
}
.pricing-table th:first-child {
text-align: left;
}
.pricing-table td {
text-align: center;
}
.pricing-table td:first-child {
text-align: left;
color: var(--color-text-muted);
}
.pricing-table tbody tr:last-child td {
border-bottom: none;
}
.pricing-table tfoot td {
border-bottom: none;
padding-top: var(--size-l);
}
.plan-header {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--size-xs);
}
.plan-name {
font-size: var(--font-size-lg);
}
.plan-price {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
}
.plan-period {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
font-weight: var(--font-weight-normal);
}
.plan-featured .plan-header {
color: var(--color-interactive);
}
.feature-check {
color: var(--color-success);
}
.feature-unavailable {
color: var(--color-text-muted);
}
.category-row td {
background: var(--color-surface);
font-weight: var(--font-weight-semibold);
color: var(--color-text);
text-align: left !important;
}
With Billing Toggle
Pricing cards with a monthly/yearly billing toggle. Uses the CSS :has() selector to show different prices based on the toggle state, requiring no JavaScript for core functionality.
<section class="pricing-section" data-layout="center" data-layout-max="wide" data-layout-gap="2xl">
<header data-layout="stack" data-layout-gap="m">
<h2>Simple, transparent pricing</h2>
<p class="text-muted">Choose the plan that's right for you</p>
<div class="pricing-toggle-wrapper">
<div class="pricing-toggle">
<span class="toggle-label active" id="monthly-label">Monthly</span>
<label class="toggle-switch">
<input type="checkbox" aria-labelledby="monthly-label yearly-label" />
<span class="toggle-slider"></span>
</label>
<span class="toggle-label" id="yearly-label">Yearly</span>
<span class="savings-badge">Save 20%</span>
</div>
</div>
</header>
<div data-layout="grid" data-layout-min="280px" data-layout-gap="l" class="pricing-cards">
<article class="pricing-card" data-layout="stack" data-layout-gap="l">
<div data-layout="stack" data-layout-gap="xs">
<span class="plan-name">Basic</span>
<div>
<span class="price price-monthly">$9</span>
<span class="price price-yearly">$7</span>
<span class="price-period">/month</span>
</div>
</div>
<!-- features... -->
</article>
<!-- more cards... -->
</div>
</section>
Required CSS
Add these styles for the billing toggle. The :has() selector enables price switching without JavaScript:
.pricing-toggle {
display: inline-flex;
align-items: center;
gap: var(--size-m);
}
.toggle-label {
font-weight: var(--font-weight-medium);
color: var(--color-text-muted);
transition: color 0.2s ease;
}
.toggle-label.active {
color: var(--color-text);
}
.toggle-switch {
position: relative;
width: 56px;
height: 28px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: var(--color-interactive);
border-radius: var(--radius-full);
transition: background 0.2s ease;
}
.toggle-slider::before {
content: "";
position: absolute;
height: 22px;
width: 22px;
left: 3px;
bottom: 3px;
background: var(--color-white);
border-radius: var(--radius-full);
transition: transform 0.2s ease;
}
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(28px);
}
.toggle-switch input:focus-visible + .toggle-slider {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.savings-badge {
background: var(--color-success-subtle);
color: var(--color-success);
padding: var(--size-2xs) var(--size-s);
border-radius: var(--radius-full);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
}
/* Base hidden state */
.price-monthly,
.price-yearly {
display: none;
}
/* Price visibility using :has() */
.pricing-toggle-wrapper:has(input:not(:checked)) ~ .pricing-cards .price-monthly {
display: inline;
}
.pricing-toggle-wrapper:has(input:checked) ~ .pricing-cards .price-yearly {
display: inline;
}
Optional JavaScript Enhancement
The core price-switching is CSS-only via :has(). The demo includes a small script for visual label feedback, toggling the .active class on the Monthly/Yearly labels as the user interacts with the toggle:
// Optional: Update toggle label states for visual feedback
const toggle = document.querySelector('.toggle-switch input');
const monthlyLabel = document.getElementById('monthly-label');
const yearlyLabel = document.getElementById('yearly-label');
toggle.addEventListener('change', () => {
monthlyLabel.classList.toggle('active', !toggle.checked);
yearlyLabel.classList.toggle('active', toggle.checked);
});
Layout Configuration
Common data attributes for pricing sections:
Element
Attribute
Values
Description
data-layout="grid"
data-layout-min
CSS length (e.g., 280px)
Minimum card width before wrapping.
data-layout="grid"
data-layout-gap
xs s m l xl
Gap between grid items.
data-layout="center"
data-layout-max
narrow measure wide
Maximum width of pricing section.
data-layout="stack"
data-layout-gap
xs s m l xl 2xl
Vertical spacing between elements.
.pricing-card
data-featured
Boolean attribute
Highlights the recommended plan.
Usage Notes
Tier naming: Use clear, hierarchical names (Basic, Pro, Enterprise) that indicate value progression.
Feature ordering: List features in order of importance. Lead with the most compelling features.
Highlight recommended: Always highlight one tier as recommended. This guides user decisions and improves conversion.
CTAs: Use primary buttons for the recommended plan and secondary buttons for others. Use "Contact Sales" for enterprise tiers.
Pricing transparency: Show all costs upfront. If there are per-user or usage-based costs, make them clear.
Yearly savings: When offering discounts for annual billing, display the savings prominently (e.g., "Save 20%").
Comparison tables: Use tables for detailed feature comparisons. Cards work better for quick overviews.
Mobile considerations: Cards stack naturally on mobile. Tables may need horizontal scrolling.
Accessibility: Use proper table semantics with scope attributes. Ensure toggle has proper labeling.