21 CSS Pricing Tables14 / 21

CSS + JSMIT licensed

Multi-Product Bundle Checkbox Matrix

A build-your-own-bundle calculator: tick add-ons on and off and the order summary re-tallies live, each line item sliding in, the total ticking over with a pop, and a bundle discount unlocking automatically at three or more add-ons.

Published

Live Demo
Try it

The code

<section class="prc-14" aria-label="Build your bundle pricing calculator">
  <div class="prc-14__wrap">
    <div class="prc-14__pick">
      <h2>Build your bundle</h2>
      <p class="prc-14__sub">Base platform $29/mo · add what you need · 3+ add-ons save 10%</p>
      <ul class="prc-14__list">
        <li><input type="checkbox" id="prc-14-a" data-price="12" data-name="Analytics"><label for="prc-14-a"><span>Analytics</span><b>$12</b></label></li>
        <li><input type="checkbox" id="prc-14-b" data-price="9" data-name="Automations"><label for="prc-14-b"><span>Automations</span><b>$9</b></label></li>
        <li><input type="checkbox" id="prc-14-c" data-price="15" data-name="AI Assist"><label for="prc-14-c"><span>AI Assist</span><b>$15</b></label></li>
        <li><input type="checkbox" id="prc-14-d" data-price="7" data-name="Forms"><label for="prc-14-d"><span>Forms</span><b>$7</b></label></li>
        <li><input type="checkbox" id="prc-14-e" data-price="19" data-name="White-label"><label for="prc-14-e"><span>White-label</span><b>$19</b></label></li>
      </ul>
    </div>
    <aside class="prc-14__summary" aria-live="polite">
      <h3>Your plan</h3>
      <ul class="prc-14__lines" id="prc-14-lines">
        <li class="prc-14__line prc-14__line--base prc-14__on"><span>Platform</span><b>$29</b></li>
        <li class="prc-14__line" data-for="prc-14-a"><span>Analytics</span><b>$12</b></li>
        <li class="prc-14__line" data-for="prc-14-b"><span>Automations</span><b>$9</b></li>
        <li class="prc-14__line" data-for="prc-14-c"><span>AI Assist</span><b>$15</b></li>
        <li class="prc-14__line" data-for="prc-14-d"><span>Forms</span><b>$7</b></li>
        <li class="prc-14__line" data-for="prc-14-e"><span>White-label</span><b>$19</b></li>
        <li class="prc-14__line prc-14__line--disc" id="prc-14-disc"><span>Bundle −10%</span><b id="prc-14-disc-amt">−$0</b></li>
      </ul>
      <p class="prc-14__total"><span>Total</span><output id="prc-14-total">$29</output><small>/mo</small></p>
      <a href="#" class="prc-14__cta">Start with this bundle</a>
    </aside>
  </div>
</section>
.prc-14,
.prc-14 *,
.prc-14 *::before,
.prc-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.prc-14 {
  --accent: oklch(0.63 0.19 30);
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #fbf7f4;
  color: #241a15;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 56px 24px;
}

.prc-14__wrap {
  display: grid;
  grid-template-columns: 1.2fr .9fr;
  gap: 28px;
  width: min(860px,100%);
}

@media (max-width: 680px) {
  .prc-14__wrap {
    grid-template-columns: 1fr;
  }
}

.prc-14__pick h2 {
  font-size: clamp(24px,3.4vw,32px);
  font-weight: 800;
  letter-spacing: -.02em;
}

.prc-14__sub {
  font-size: 13.5px;
  color: #7d6f65;
  margin: 8px 0 22px;
}

.prc-14__list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

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

.prc-14__list label {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  background: #fff;
  border: 1.5px solid #eadfd7;
  border-radius: 13px;
  padding: 14px 18px;
  font-size: 14.5px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color .25s,background .25s,transform .15s;
}

.prc-14__list label b {
  font-variant-numeric: tabular-nums;
  color: #7d6f65;
}

.prc-14__list label span::before {
  content: "";
  display: inline-block;
  width: 18px;
  height: 18px;
  border-radius: 6px;
  border: 2px solid #d8cabf;
  margin-right: 12px;
  vertical-align: -4px;
  transition: background .2s,border-color .2s;
  background: center/12px no-repeat;
}

.prc-14__list li:has(input:checked) label {
  border-color: var(--accent);
  background: oklch(0.63 0.19 30/.06);
}

.prc-14__list li:has(input:checked) label span::before {
  background-color: var(--accent);
  border-color: var(--accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 6.5 4.8 9 10 3.5' fill='none' stroke='white' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E");
}

.prc-14__list label:active {
  transform: scale(.985);
}

.prc-14__list input:focus-visible + label {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.prc-14__summary {
  background: #241a15;
  color: #f5ede7;
  border-radius: 20px;
  padding: 26px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  height: max-content;
}

.prc-14__summary h3 {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #b3a294;
}

.prc-14__lines {
  list-style: none;
  display: flex;
  flex-direction: column;
}

.prc-14__line {
  display: flex;
  justify-content: space-between;
  font-size: 13.5px;
  height: 0;
  opacity: 0;
  transform: translateX(-6px);
  overflow: hidden;
  transition: height .3s,opacity .3s,transform .3s;
}

.prc-14__line b {
  font-variant-numeric: tabular-nums;
}

.prc-14__line.prc-14__on {
  height: 26px;
  opacity: 1;
  transform: none;
}

.prc-14__line--disc {
  color: oklch(0.8 0.13 145);
}

.prc-14__total {
  display: flex;
  align-items: baseline;
  gap: 8px;
  border-top: 1px solid #4a3a2f;
  padding-top: 14px;
  font-size: 14px;
  font-weight: 600;
}

.prc-14__total output {
  margin-left: auto;
  font-size: 32px;
  font-weight: 800;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

.prc-14__total output.prc-14__pop {
  animation: prc-14-pop .35s cubic-bezier(.3,1.6,.4,1);
}

@keyframes prc-14-pop {
  40% {
    transform: scale(1.14);
    color: oklch(0.78 0.15 30);
  }
}

.prc-14__total small {
  color: #b3a294;
}

.prc-14__cta {
  text-align: center;
  text-decoration: none;
  font-weight: 700;
  font-size: 13.5px;
  padding: 13px;
  border-radius: 11px;
  background: var(--accent);
  color: #fff;
  transition: filter .2s;
}

.prc-14__cta:hover {
  filter: brightness(1.1);
}

.prc-14__cta:focus-visible {
  outline: 3px solid #f5ede7;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .prc-14__line,
    .prc-14__list label {
    transition-duration: .01s;
  }

  .prc-14__total output.prc-14__pop {
    animation: none;
  }
}
(() => {
  const root = document.querySelector('.prc-14');
  const boxes = [...root.querySelectorAll('.prc-14__list input')];
  const total = root.querySelector('#prc-14-total');
  const disc = root.querySelector('#prc-14-disc');
  const discAmt = root.querySelector('#prc-14-disc-amt');
  const BASE = 29, DISC_AT = 3, DISC_PCT = 0.10;
  function update() {
    const picked = boxes.filter(b => b.checked);
    boxes.forEach(b => {
      root.querySelector('[data-for="' + b.id + '"]').classList.toggle('prc-14__on', b.checked);
    });
    let sum = BASE + picked.reduce((s, b) => s + +b.dataset.price, 0);
    const hasDisc = picked.length >= DISC_AT;
    const off = hasDisc ? Math.round(sum * DISC_PCT) : 0;
    disc.classList.toggle('prc-14__on', hasDisc);
    discAmt.textContent = '−$' + off;
    total.textContent = '$' + (sum - off);
    total.classList.remove('prc-14__pop');
    requestAnimationFrame(() => requestAnimationFrame(() => total.classList.add('prc-14__pop')));
  }
  boxes.forEach(b => b.addEventListener('change', update));
})();
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: Multi-Product Bundle Checkbox Matrix
Source: https://codefronts.com/components/css-pricing-tables/multi-product-bundle-checkbox-matrix/

A build-your-own-bundle calculator: tick add-ons on and off and the order summary re-tallies live, each line item sliding in, the total ticking over with a pop, and a bundle discount unlocking automatically at three or more add-ons.
## HTML
```html
<section class="prc-14" aria-label="Build your bundle pricing calculator">
  <div class="prc-14__wrap">
    <div class="prc-14__pick">
      <h2>Build your bundle</h2>
      <p class="prc-14__sub">Base platform $29/mo · add what you need · 3+ add-ons save 10%</p>
      <ul class="prc-14__list">
        <li><input type="checkbox" id="prc-14-a" data-price="12" data-name="Analytics"><label for="prc-14-a"><span>Analytics</span><b>$12</b></label></li>
        <li><input type="checkbox" id="prc-14-b" data-price="9" data-name="Automations"><label for="prc-14-b"><span>Automations</span><b>$9</b></label></li>
        <li><input type="checkbox" id="prc-14-c" data-price="15" data-name="AI Assist"><label for="prc-14-c"><span>AI Assist</span><b>$15</b></label></li>
        <li><input type="checkbox" id="prc-14-d" data-price="7" data-name="Forms"><label for="prc-14-d"><span>Forms</span><b>$7</b></label></li>
        <li><input type="checkbox" id="prc-14-e" data-price="19" data-name="White-label"><label for="prc-14-e"><span>White-label</span><b>$19</b></label></li>
      </ul>
    </div>
    <aside class="prc-14__summary" aria-live="polite">
      <h3>Your plan</h3>
      <ul class="prc-14__lines" id="prc-14-lines">
        <li class="prc-14__line prc-14__line--base prc-14__on"><span>Platform</span><b>$29</b></li>
        <li class="prc-14__line" data-for="prc-14-a"><span>Analytics</span><b>$12</b></li>
        <li class="prc-14__line" data-for="prc-14-b"><span>Automations</span><b>$9</b></li>
        <li class="prc-14__line" data-for="prc-14-c"><span>AI Assist</span><b>$15</b></li>
        <li class="prc-14__line" data-for="prc-14-d"><span>Forms</span><b>$7</b></li>
        <li class="prc-14__line" data-for="prc-14-e"><span>White-label</span><b>$19</b></li>
        <li class="prc-14__line prc-14__line--disc" id="prc-14-disc"><span>Bundle −10%</span><b id="prc-14-disc-amt">−$0</b></li>
      </ul>
      <p class="prc-14__total"><span>Total</span><output id="prc-14-total">$29</output><small>/mo</small></p>
      <a href="#" class="prc-14__cta">Start with this bundle</a>
    </aside>
  </div>
</section>
```
## CSS
```css
.prc-14,
.prc-14 *,
.prc-14 *::before,
.prc-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.prc-14 {
  --accent: oklch(0.63 0.19 30);
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #fbf7f4;
  color: #241a15;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 56px 24px;
}

.prc-14__wrap {
  display: grid;
  grid-template-columns: 1.2fr .9fr;
  gap: 28px;
  width: min(860px,100%);
}

@media (max-width: 680px) {
  .prc-14__wrap {
    grid-template-columns: 1fr;
  }
}

.prc-14__pick h2 {
  font-size: clamp(24px,3.4vw,32px);
  font-weight: 800;
  letter-spacing: -.02em;
}

.prc-14__sub {
  font-size: 13.5px;
  color: #7d6f65;
  margin: 8px 0 22px;
}

.prc-14__list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

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

.prc-14__list label {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  background: #fff;
  border: 1.5px solid #eadfd7;
  border-radius: 13px;
  padding: 14px 18px;
  font-size: 14.5px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color .25s,background .25s,transform .15s;
}

.prc-14__list label b {
  font-variant-numeric: tabular-nums;
  color: #7d6f65;
}

.prc-14__list label span::before {
  content: "";
  display: inline-block;
  width: 18px;
  height: 18px;
  border-radius: 6px;
  border: 2px solid #d8cabf;
  margin-right: 12px;
  vertical-align: -4px;
  transition: background .2s,border-color .2s;
  background: center/12px no-repeat;
}

.prc-14__list li:has(input:checked) label {
  border-color: var(--accent);
  background: oklch(0.63 0.19 30/.06);
}

.prc-14__list li:has(input:checked) label span::before {
  background-color: var(--accent);
  border-color: var(--accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 6.5 4.8 9 10 3.5' fill='none' stroke='white' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E");
}

.prc-14__list label:active {
  transform: scale(.985);
}

.prc-14__list input:focus-visible + label {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.prc-14__summary {
  background: #241a15;
  color: #f5ede7;
  border-radius: 20px;
  padding: 26px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  height: max-content;
}

.prc-14__summary h3 {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #b3a294;
}

.prc-14__lines {
  list-style: none;
  display: flex;
  flex-direction: column;
}

.prc-14__line {
  display: flex;
  justify-content: space-between;
  font-size: 13.5px;
  height: 0;
  opacity: 0;
  transform: translateX(-6px);
  overflow: hidden;
  transition: height .3s,opacity .3s,transform .3s;
}

.prc-14__line b {
  font-variant-numeric: tabular-nums;
}

.prc-14__line.prc-14__on {
  height: 26px;
  opacity: 1;
  transform: none;
}

.prc-14__line--disc {
  color: oklch(0.8 0.13 145);
}

.prc-14__total {
  display: flex;
  align-items: baseline;
  gap: 8px;
  border-top: 1px solid #4a3a2f;
  padding-top: 14px;
  font-size: 14px;
  font-weight: 600;
}

.prc-14__total output {
  margin-left: auto;
  font-size: 32px;
  font-weight: 800;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

.prc-14__total output.prc-14__pop {
  animation: prc-14-pop .35s cubic-bezier(.3,1.6,.4,1);
}

@keyframes prc-14-pop {
  40% {
    transform: scale(1.14);
    color: oklch(0.78 0.15 30);
  }
}

.prc-14__total small {
  color: #b3a294;
}

.prc-14__cta {
  text-align: center;
  text-decoration: none;
  font-weight: 700;
  font-size: 13.5px;
  padding: 13px;
  border-radius: 11px;
  background: var(--accent);
  color: #fff;
  transition: filter .2s;
}

.prc-14__cta:hover {
  filter: brightness(1.1);
}

.prc-14__cta:focus-visible {
  outline: 3px solid #f5ede7;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .prc-14__line,
    .prc-14__list label {
    transition-duration: .01s;
  }

  .prc-14__total output.prc-14__pop {
    animation: none;
  }
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('.prc-14');
  const boxes = [...root.querySelectorAll('.prc-14__list input')];
  const total = root.querySelector('#prc-14-total');
  const disc = root.querySelector('#prc-14-disc');
  const discAmt = root.querySelector('#prc-14-disc-amt');
  const BASE = 29, DISC_AT = 3, DISC_PCT = 0.10;
  function update() {
    const picked = boxes.filter(b => b.checked);
    boxes.forEach(b => {
      root.querySelector('[data-for="' + b.id + '"]').classList.toggle('prc-14__on', b.checked);
    });
    let sum = BASE + picked.reduce((s, b) => s + +b.dataset.price, 0);
    const hasDisc = picked.length >= DISC_AT;
    const off = hasDisc ? Math.round(sum * DISC_PCT) : 0;
    disc.classList.toggle('prc-14__on', hasDisc);
    discAmt.textContent = '−$' + off;
    total.textContent = '$' + (sum - off);
    total.classList.remove('prc-14__pop');
    requestAnimationFrame(() => requestAnimationFrame(() => total.classList.add('prc-14__pop')));
  }
  boxes.forEach(b => b.addEventListener('change', update));
})();
```

How this works

Each add-on row is a real checkbox+label; checking it highlights the row via :has() and JS re-renders only the summary column: line items toggle a class that transitions height-free (opacity/transform in a grid of fixed rows), and the total <output> gets a re-triggered prc-14-pop animation by toggling the class between frames with requestAnimationFrame.

The 10% bundle discount is pure derived state — three or more checked boxes adds a discount line and restyles the total. The summary is an aria-live="polite" region announcing the new total once per change.

Make it yours

  • Add-ons and prices live in data-price attributes — add rows freely.
  • Discount rule (count and percent) is two constants in the JS.
  • Pop intensity: the prc-14-pop keyframe scale.
  • Currency formatting via the fmt() Intl call.

Gotchas — read before shipping

  • Re-trigger CSS animations by removing the class, forcing a frame (rAF), then re-adding — resetting animation-name inline is flakier.
  • Keep line items in fixed-height rows so the summary doesn't jump as items toggle.
  • Announce totals, not every keystroke of internal state, to aria-live.

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+.

:has() row highlighting is cosmetic; the calculator itself runs on universal JS + checkboxes and works back to IE-era baselines.

Techniques used in this demo

Search CodeFronts

Loading…