21 CSS Pricing Tables01 / 21

Pure CSSMIT licensed

Monthly / Annual Toggle Switch

The single most-copied SaaS pattern: a pill toggle slides between Monthly and Annual billing, and every price on the 3-card grid flips to its discounted yearly value with a smooth vertical roll — zero JavaScript, driven entirely by :has() and a checkbox.

Published

Live Demo
Try it

The code

<section class="prc-01" aria-label="Pricing with monthly and annual billing toggle">
  <header class="prc-01__head">
    <h2>Simple, honest pricing</h2>
    <div class="prc-01__toggle">
      <span id="prc-01-mo-label">Monthly</span>
      <input type="checkbox" id="prc-01-bill" class="prc-01__sr" aria-label="Bill annually and save 20%">
      <label for="prc-01-bill" class="prc-01__pill" aria-hidden="true"><span class="prc-01__knob"></span></label>
      <span id="prc-01-yr-label">Annual</span>
      <span class="prc-01__save">Save 20%</span>
    </div>
  </header>
  <div class="prc-01__grid">
    <article class="prc-01__card">
      <h3>Starter</h3>
      <div class="prc-01__prices"><span class="prc-01__price prc-01__price--mo">$12<small>/mo</small></span><span class="prc-01__price prc-01__price--yr">$9<small>/mo</small></span></div>
      <ul><li>3 projects</li><li>2 GB storage</li><li>Community support</li></ul>
      <a href="#" class="prc-01__cta">Start free</a>
    </article>
    <article class="prc-01__card prc-01__card--hot">
      <h3>Pro</h3>
      <div class="prc-01__prices"><span class="prc-01__price prc-01__price--mo">$29<small>/mo</small></span><span class="prc-01__price prc-01__price--yr">$23<small>/mo</small></span></div>
      <ul><li>Unlimited projects</li><li>200 GB storage</li><li>Priority support</li><li>API access</li></ul>
      <a href="#" class="prc-01__cta prc-01__cta--hot">Choose Pro</a>
    </article>
    <article class="prc-01__card">
      <h3>Team</h3>
      <div class="prc-01__prices"><span class="prc-01__price prc-01__price--mo">$79<small>/mo</small></span><span class="prc-01__price prc-01__price--yr">$63<small>/mo</small></span></div>
      <ul><li>Everything in Pro</li><li>SSO &amp; audit log</li><li>Dedicated CSM</li></ul>
      <a href="#" class="prc-01__cta">Contact sales</a>
    </article>
  </div>
</section>
.prc-01,
.prc-01 *,
.prc-01 *::before,
.prc-01 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.prc-01 {
  --accent: oklch(0.58 0.19 264);
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #f6f7fb;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 56px 24px;
  color: #171a26;
}

.prc-01__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.prc-01__head {
  text-align: center;
  margin-bottom: 40px;
}

.prc-01__head h2 {
  font-size: clamp(26px,4vw,36px);
  font-weight: 800;
  letter-spacing: -.02em;
  margin-bottom: 20px;
}

.prc-01__toggle {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 14px;
  font-weight: 600;
  color: #5a6074;
}

.prc-01__pill {
  width: 52px;
  height: 28px;
  border-radius: 99px;
  background: #d8dbe6;
  cursor: pointer;
  position: relative;
  transition: background .3s;
}

.prc-01__knob {
  position: absolute;
  inset: 3px auto 3px 3px;
  width: 22px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(20,24,50,.25);
  transition: transform .3s cubic-bezier(.5,1.6,.4,1);
}

.prc-01:has(.prc-01__sr:checked) .prc-01__pill {
  background: var(--accent);
}

.prc-01:has(.prc-01__sr:checked) .prc-01__knob {
  transform: translateX(24px);
}

.prc-01__sr:focus-visible + .prc-01__pill {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.prc-01__save {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 99px;
  background: oklch(0.93 0.05 150);
  color: oklch(0.45 0.12 150);
  opacity: .45;
  transform: scale(.94);
  transition: opacity .3s,transform .3s;
}

.prc-01:has(.prc-01__sr:checked) .prc-01__save {
  opacity: 1;
  transform: scale(1);
}

.prc-01__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(230px,1fr));
  gap: 20px;
  width: min(920px,100%);
  margin: 0 auto;
}

.prc-01__card {
  background: #fff;
  border: 1px solid #e4e7f0;
  border-radius: 18px;
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.prc-01__card--hot {
  border-color: var(--accent);
  box-shadow: 0 18px 44px -24px oklch(0.58 0.19 264 / .5);
}

.prc-01__card h3 {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: #5a6074;
}

.prc-01__prices {
  display: grid;
  height: 52px;
  overflow: hidden;
}

.prc-01__price {
  grid-area: 1/1;
  font-size: 42px;
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: 52px;
  transition: transform .45s cubic-bezier(.6,0,.2,1),opacity .3s;
}

.prc-01__price small {
  font-size: 15px;
  font-weight: 600;
  color: #8a90a4;
  letter-spacing: 0;
}

.prc-01__price--yr {
  transform: translateY(110%);
  opacity: 0;
}

.prc-01:has(.prc-01__sr:checked) .prc-01__price--mo {
  transform: translateY(-110%);
  opacity: 0;
}

.prc-01:has(.prc-01__sr:checked) .prc-01__price--yr {
  transform: translateY(0);
  opacity: 1;
}

.prc-01__card ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 9px;
  font-size: 14px;
  color: #464c60;
  flex: 1;
}

.prc-01__card li::before {
  content: "✓";
  color: var(--accent);
  font-weight: 700;
  margin-right: 9px;
}

.prc-01__cta {
  text-align: center;
  text-decoration: none;
  font-weight: 700;
  font-size: 14px;
  padding: 12px;
  border-radius: 11px;
  border: 1.5px solid #d8dbe6;
  color: #171a26;
  transition: border-color .2s,background .2s;
}

.prc-01__cta:hover {
  border-color: var(--accent);
}

.prc-01__cta--hot {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.prc-01__cta--hot:hover {
  background: oklch(0.52 0.19 264);
}

.prc-01__cta:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .prc-01 .prc-01__price,
    .prc-01 .prc-01__knob,
    .prc-01 .prc-01__save {
    transition-duration: .01s;
  }
}
/* No JavaScript — a hidden checkbox + :has() drives the billing state; both prices share one grid cell and roll via transform/opacity, so the swap is compositor-only with zero layout shift. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Pricing Table from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Monthly / Annual Toggle Switch
Source: https://codefronts.com/components/css-pricing-tables/monthly-annual-toggle-switch/

The single most-copied SaaS pattern: a pill toggle slides between Monthly and Annual billing, and every price on the 3-card grid flips to its discounted yearly value with a smooth vertical roll — zero JavaScript, driven entirely by :has() and a checkbox.
## HTML
```html
<section class="prc-01" aria-label="Pricing with monthly and annual billing toggle">
  <header class="prc-01__head">
    <h2>Simple, honest pricing</h2>
    <div class="prc-01__toggle">
      <span id="prc-01-mo-label">Monthly</span>
      <input type="checkbox" id="prc-01-bill" class="prc-01__sr" aria-label="Bill annually and save 20%">
      <label for="prc-01-bill" class="prc-01__pill" aria-hidden="true"><span class="prc-01__knob"></span></label>
      <span id="prc-01-yr-label">Annual</span>
      <span class="prc-01__save">Save 20%</span>
    </div>
  </header>
  <div class="prc-01__grid">
    <article class="prc-01__card">
      <h3>Starter</h3>
      <div class="prc-01__prices"><span class="prc-01__price prc-01__price--mo">$12<small>/mo</small></span><span class="prc-01__price prc-01__price--yr">$9<small>/mo</small></span></div>
      <ul><li>3 projects</li><li>2 GB storage</li><li>Community support</li></ul>
      <a href="#" class="prc-01__cta">Start free</a>
    </article>
    <article class="prc-01__card prc-01__card--hot">
      <h3>Pro</h3>
      <div class="prc-01__prices"><span class="prc-01__price prc-01__price--mo">$29<small>/mo</small></span><span class="prc-01__price prc-01__price--yr">$23<small>/mo</small></span></div>
      <ul><li>Unlimited projects</li><li>200 GB storage</li><li>Priority support</li><li>API access</li></ul>
      <a href="#" class="prc-01__cta prc-01__cta--hot">Choose Pro</a>
    </article>
    <article class="prc-01__card">
      <h3>Team</h3>
      <div class="prc-01__prices"><span class="prc-01__price prc-01__price--mo">$79<small>/mo</small></span><span class="prc-01__price prc-01__price--yr">$63<small>/mo</small></span></div>
      <ul><li>Everything in Pro</li><li>SSO &amp; audit log</li><li>Dedicated CSM</li></ul>
      <a href="#" class="prc-01__cta">Contact sales</a>
    </article>
  </div>
</section>
```
## CSS
```css
.prc-01,
.prc-01 *,
.prc-01 *::before,
.prc-01 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.prc-01 {
  --accent: oklch(0.58 0.19 264);
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #f6f7fb;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 56px 24px;
  color: #171a26;
}

.prc-01__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.prc-01__head {
  text-align: center;
  margin-bottom: 40px;
}

.prc-01__head h2 {
  font-size: clamp(26px,4vw,36px);
  font-weight: 800;
  letter-spacing: -.02em;
  margin-bottom: 20px;
}

.prc-01__toggle {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 14px;
  font-weight: 600;
  color: #5a6074;
}

.prc-01__pill {
  width: 52px;
  height: 28px;
  border-radius: 99px;
  background: #d8dbe6;
  cursor: pointer;
  position: relative;
  transition: background .3s;
}

.prc-01__knob {
  position: absolute;
  inset: 3px auto 3px 3px;
  width: 22px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(20,24,50,.25);
  transition: transform .3s cubic-bezier(.5,1.6,.4,1);
}

.prc-01:has(.prc-01__sr:checked) .prc-01__pill {
  background: var(--accent);
}

.prc-01:has(.prc-01__sr:checked) .prc-01__knob {
  transform: translateX(24px);
}

.prc-01__sr:focus-visible + .prc-01__pill {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.prc-01__save {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 99px;
  background: oklch(0.93 0.05 150);
  color: oklch(0.45 0.12 150);
  opacity: .45;
  transform: scale(.94);
  transition: opacity .3s,transform .3s;
}

.prc-01:has(.prc-01__sr:checked) .prc-01__save {
  opacity: 1;
  transform: scale(1);
}

.prc-01__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(230px,1fr));
  gap: 20px;
  width: min(920px,100%);
  margin: 0 auto;
}

.prc-01__card {
  background: #fff;
  border: 1px solid #e4e7f0;
  border-radius: 18px;
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.prc-01__card--hot {
  border-color: var(--accent);
  box-shadow: 0 18px 44px -24px oklch(0.58 0.19 264 / .5);
}

.prc-01__card h3 {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: #5a6074;
}

.prc-01__prices {
  display: grid;
  height: 52px;
  overflow: hidden;
}

.prc-01__price {
  grid-area: 1/1;
  font-size: 42px;
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: 52px;
  transition: transform .45s cubic-bezier(.6,0,.2,1),opacity .3s;
}

.prc-01__price small {
  font-size: 15px;
  font-weight: 600;
  color: #8a90a4;
  letter-spacing: 0;
}

.prc-01__price--yr {
  transform: translateY(110%);
  opacity: 0;
}

.prc-01:has(.prc-01__sr:checked) .prc-01__price--mo {
  transform: translateY(-110%);
  opacity: 0;
}

.prc-01:has(.prc-01__sr:checked) .prc-01__price--yr {
  transform: translateY(0);
  opacity: 1;
}

.prc-01__card ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 9px;
  font-size: 14px;
  color: #464c60;
  flex: 1;
}

.prc-01__card li::before {
  content: "✓";
  color: var(--accent);
  font-weight: 700;
  margin-right: 9px;
}

.prc-01__cta {
  text-align: center;
  text-decoration: none;
  font-weight: 700;
  font-size: 14px;
  padding: 12px;
  border-radius: 11px;
  border: 1.5px solid #d8dbe6;
  color: #171a26;
  transition: border-color .2s,background .2s;
}

.prc-01__cta:hover {
  border-color: var(--accent);
}

.prc-01__cta--hot {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.prc-01__cta--hot:hover {
  background: oklch(0.52 0.19 264);
}

.prc-01__cta:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .prc-01 .prc-01__price,
    .prc-01 .prc-01__knob,
    .prc-01 .prc-01__save {
    transition-duration: .01s;
  }
}
```

## JavaScript
```js
/* No JavaScript — a hidden checkbox + :has() drives the billing state; both prices share one grid cell and roll via transform/opacity, so the swap is compositor-only with zero layout shift. */
```

How this works

One visually-hidden checkbox sits at the top of the section; its two billing labels wrap a sliding pill whose knob moves via transform:translateX(). Each card renders both prices stacked in the same grid cell (grid-area:1/1). When the section matches :has(:checked), the monthly price rolls up and out (translateY + opacity) while the annual price rolls in — a compositor-only crossfade with no layout shift because both values occupy one reserved cell.

The "Save 20%" chip fades in with the annual state via the same :has() selector. Because the control is a real checkbox with a bound label, it is click-, tap-, Space- and screen-reader-operable for free.

Make it yours

  • Change the discount copy in the .prc-01__save chip.
  • Adjust roll distance/speed via the translateY values and transition duration.
  • Add a fourth card — the :has() logic needs no re-wiring.
  • Recolour through the --accent custom property on .prc-01.

Gotchas — read before shipping

  • Both price spans must share one grid cell (grid-area:1/1) or the swap causes layout shift.
  • Keep the checkbox focusable (sr-only clip, not display:none) so keyboard users can toggle billing.
  • :has() needs Chrome 105+ / Safari 15.4+ / Firefox 121+ — ship a JS class-toggle fallback for legacy traffic if it matters.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

Floor set by :has(), oklch() as this demo is written. The core technique itself goes back further — Chrome 105+.

Entirely dependent on :has(). Everything else (grid stacking, transforms) is universal.

Techniques used in this demo

Search CodeFronts

Loading…