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.

Related

Layout Grid

Grid layout element documentation

Layout Stack

Stack layout element documentation

Call to Action

CTA section patterns

Features

Feature grid patterns