GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 Pricing Page Design Sections (Copy-Paste)

Nine pricing sections that compute: the billing switch reworks every plan's price on the spot, the discount badge derives its own percentage, and

Auto-plays · click a tile to jump to its section · all nine in one zip

Every section below replays a fixed set of scenario arrays through setInterval, never Math.random() or a live payment call, so the same plans, the same review scores, and the same slider steps land in the same place on every capture. Clicking a question open in the FAQ accordion is the one real interaction in the set and stops that replay immediately, handing control to whoever is looking at the page; prefers-reduced-motion keeps every one of those calculations running while only dropping the transition that would otherwise animate the result into view.

Anatomy of a pricing page — what nine sections compute

Nothing here shows a number by typing it into the markup once and leaving it there. The same handful of arithmetic moves resurface across all nine sections wearing different plan names, and spotting them turns every item below into a variant of the same idea rather than nine unrelated widgets.

Why a percentage derived from two prices beats a hard-coded discount number

Dividing the gap between list price and sale price by the list price is what item 04's badge text actually is, so changing either number keeps the percentage honest instead of leaving a stale figure printed next to a price that has since moved.

Why a diff count feeds a highlight strength instead of an on-or-off flag

Counting how many of a comparison table's rows actually differ across plans and feeding that count into --diff-alpha is what lets item 02 darken a five-row difference more than a one-row difference, rather than marking every differing row at the same strength.

Why a measured pixel width decides where a handle sits, not a translateX trick

Item 07's slider and item 08's logo bar both read clientWidth or offsetWidth before doing any math, because a fixed percentage or a CSS transform alone breaks the moment the container is resized to a width nobody measured in advance. A pricing page design template that only ships a screenshot leaves all of this invisible; here every card, table, and slider keeps computing after the click.

Code per section

Each code block below is copied straight out of that item's own index.html, not retyped from memory, and the matching prompt in the zip regenerates the same file if you hand it to an AI coding tool instead. Searching for a pricing table design html css example turns up plenty of static screenshots, but every section here keeps recomputing after the first paint instead of freezing once the page loads.

01Plan cards — a card row that recomputes its price on every billing switch

Why the yearly-equivalent price is recomputed on every flip, not stored per plan

Multiplying each plan's own discount rate into its monthly price on every billing flip is what produces the yearly-equivalent figure item 01 shows, and requestAnimationFrame redraws the digits rolling from the old value to the new one along an easing curve rather than snapping straight to the answer.

Plan cards — a card row that recomputes its price on every billing switch
Open in a new tab

01 · Plan cards — a card row that recomputes its price on every billing switch — Measured area 16.259% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

requestAnimationFramearia-checkedtranslateX

What the numbers say

Cumulative area lands at 16.26% (max frame 15.97%, mean intensity 103.0) across 18 of 23 frame-to-frame comparisons as the scripted scenario cycles through billing flips.

function yearlyMonthlyEq(plan) {
  return Math.round(plan.monthly * (1 - plan.discount));
}

function animateValue(el, from, to, duration) {
  if (!el) return;
  if (reduce || from === to) { el.textContent = String(to); return; }
  var start = null;
  function step(ts) {
    if (start === null) start = ts;
    var p = Math.min((ts - start) / duration, 1);
    var eased = 1 - Math.pow(1 - p, 3);
    el.textContent = String(Math.round(from + (to - from) * eased));
    if (p < 1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

02Comparison table — a grid that counts and highlights only the rows that actually differ

Why only the differing rows raise --diff-alpha, not the whole table at once

Comparing each feature's value across three plans and counting only the rows that actually differ is what diffCount is, and dividing that count by the total row count is the ratio item 02 feeds into --diff-alpha so only the rows with a real difference get darkened.

Comparison table — a grid that counts and highlights only the rows that actually differ
Open in a new tab

02 · Comparison table — a grid that counts and highlights only the rows that actually differ — Measured area 29.956% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

--diff-alphaquerySelectorAllscope="row"

What the numbers say

Cumulative area is the second-widest of the nine at 29.96% (max frame 29.88%), the lowest mean intensity of the set at 39.6, over just 7 of 23 frames as the scenario cycles three times.

function allEqual(arr) {
  return arr.every(function (v) { return v === arr[0]; });
}

function apply(scenario) {
  var diffCount = 0;
  ROW_KEYS.forEach(function (key) {
    var values = scenario[key];
    var tr = rowsEl.querySelector('tr[data-row="' + key + '"]');
    if (!tr) return;
    var cells = tr.querySelectorAll('.v');
    cells.forEach(function (cell, i) { cell.textContent = values[i]; });
    var differs = !allEqual(values);
    if (differs) diffCount += 1;
    tr.classList.toggle('is-diff', differs);
    tr.style.setProperty('--diff-alpha', differs ? String(.12 + (diffCount / ROW_KEYS.length) * .35) : '0');
  });
  chip.textContent = 'rows differ ' + diffCount + '/' + ROW_KEYS.length;
}

03Billing toggle — a switch that derives its own savings badge from its position

Why the savings figure is computed the instant the switch lands on yearly

The moment the switch lands on yearly, multiplying the monthly price currently on screen by 12 and by the yearly discount works out a year's savings on the spot and fills it into the badge text, and flipping back to monthly hides that badge again rather than leaving a stale number visible.

Billing toggle — a switch that derives its own savings badge from its position
Open in a new tab

03 · Billing toggle — a switch that derives its own savings badge from its position — Measured area 5.264% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

aria-checkedrole="switch":not([hidden])

What the numbers say

Cumulative area sits at 5.26% (max frame 5.16%, mean intensity 99.6) over 10 of 23 frames as the scripted steps flip between monthly and yearly at two different price points.

function apply(step) {
  priceNum.textContent = String(step.monthly);
  toggle.setAttribute('aria-checked', step.yearly ? 'true' : 'false');
  modeMonthly.classList.toggle('is-active', !step.yearly);
  modeYearly.classList.toggle('is-active', step.yearly);
  if (step.yearly) {
    var savings = Math.round(step.monthly * 12 * DISCOUNT);
    badge.hidden = false;
    badge.textContent = 'Yearly ' + savings + ' saved';
  } else {
    badge.hidden = true;
  }
}

04Discount badge — a badge that works out its own percentage from list price and sale price

Why the discount percentage is derived from two prices, not typed as a fixed number

Dividing the gap between list price and sale price by the list price works out the percentage item 04's badge prints, and a compressed loop stands in for a 12-minute countdown, so dropping under a threshold ratio flips the badge to its urgent color without ever reading Date.now().

Discount badge — a badge that works out its own percentage from list price and sale price
Open in a new tab

04 · Discount badge — a badge that works out its own percentage from list price and sale price — Measured area 1.447% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

requestAnimationFrameMath.flooraria-live

What the numbers say

Cumulative area is the smallest of the nine at 1.45% (max frame 1.37%, mean intensity 97.3), the only item moving on all 23 of 23 frames because the countdown redraws almost every tick.

function format(sec) {
  var m = Math.floor(sec / 60);
  var s = Math.floor(sec % 60);
  return m + ':' + (s < 10 ? '0' + s : String(s));
}

function frame(elapsed) {
  var progress = Math.min(elapsed / LOOP_MS, 1);
  var remainSec = TOTAL_SEC * (1 - progress);
  countdown.textContent = format(remainSec) + ' left';
  var urgent = (remainSec / TOTAL_SEC) < URGENT_RATIO;
  pctBadge.classList.toggle('is-urgent', urgent);
  countdown.classList.toggle('is-urgent', urgent);
  if (progress >= 1) return LOOP_MS;
  return null;
}

05Pricing FAQ — an accordion that remeasures the list height by how many entries are open

Why grid-template-rows briefly returns to auto before every open or close

Releasing grid-template-rows to auto for a moment to measure and cache each panel's real scrollHeight, then restoring the transition, is what lets item 05 sum the cached heights of the open entries into a total list height without ever reading scrollHeight mid-transition, which is always wrong.

Pricing FAQ — an accordion that remeasures the list height by how many entries are open
Open in a new tab

05 · Pricing FAQ — an accordion that remeasures the list height by how many entries are open — Measured area 34.936% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

grid-template-rowsscrollHeightaria-expanded

What the numbers say

Cumulative area is the widest of the nine at 34.94% (max frame 17.58%, mean intensity 81.3) over 9 of 23 frames as three questions cycle open and closed.

function measureAndReport() {
  var total = 0;
  items.forEach(function (item, i) {
    if (item.classList.contains('is-open')) total += NATURAL_HEIGHTS[i];
  });
  chip.textContent = 'List height ' + total + 'px';
}

function setOpen(openIndex) {
  items.forEach(function (item, i) {
    var q = item.querySelector('.q');
    var panel = item.querySelector('.a');
    var open = i === openIndex;
    item.classList.toggle('is-open', open);
    if (panel) panel.style.gridTemplateRows = open ? NATURAL_HEIGHTS[i] + 'px' : '0px';
    if (q) q.setAttribute('aria-expanded', open ? 'true' : 'false');
  });
  measureAndReport();
}

06Pricing testimonials — a review group that works out its own average rating

Why the average and the card remeasurement never happen on the same frame

Reducing the array of ratings into a single average worked out to one decimal place is what fills item 06's headline number, -webkit-line-clamp caps each quote at three lines (two under a 340px width) before any of that measuring starts, and one frame after the review text changes, every card's real scrollHeight is remeasured so the largest value becomes every card's height — never on the same frame the text was swapped in.

Pricing testimonials — a review group that works out its own average rating
Open in a new tab

06 · Pricing testimonials — a review group that works out its own average rating — Measured area 19.763% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

requestAnimationFramescrollHeightline-clamp

What the numbers say

Cumulative area comes to 19.76% (max frame 8.06%, mean intensity 106.7) over 7 of 23 frames as three review sets cycle through.

function apply(set) {
  avgNum.textContent = average(set.reviews).toFixed(1);
  avgCount.textContent = String(set.count);
  set.reviews.forEach(function (r, i) {
    quotes[i].textContent = '“' + r.text + '”';
    whos[i].textContent = r.who + ' · ' + r.rating + '★';
  });
  requestAnimationFrame(function () {
    var maxH = 0;
    cards.forEach(function (card) { maxH = Math.max(maxH, card.scrollHeight); });
    cards.forEach(function (card) { card.style.minHeight = maxH + 'px'; });
    chip.textContent = 'Card height ' + maxH + 'px';
  });
}

07Enterprise CTA — a section that turns typed usage into a live estimate

Why the handle position is computed from the track's measured width, not a CSS trick

Multiplying the usage ratio by the track's measured width minus the handle's width is what places item 07's slider handle at its real left position in pixels instead of leaning on a translateX(-50%) trick, and usage times a per-unit rate plus a base fee is recomputed into a live estimate every time the value moves.

Enterprise CTA — a section that turns typed usage into a live estimate
Open in a new tab

07 · Enterprise CTA — a section that turns typed usage into a live estimate — Measured area 2.287% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

aria-valuenowclientWidthrole="slider"

What the numbers say

Cumulative area is the second-smallest of the nine at 2.29% (max frame 0.89%, mean intensity 121.6) over 8 of 23 frames — a slider handle and an estimate line are structurally small-change surfaces.

function apply(usage) {
  var pct = usage / MAX_USAGE;
  var trackWidth = track.clientWidth;
  var knobWidth = knob.offsetWidth || 16;
  var knobLeft = pct * (trackWidth - knobWidth);
  knob.style.left = knobLeft + 'px';
  fill.style.width = (pct * 100) + '%';
  knob.setAttribute('aria-valuenow', String(usage));
  usageNum.textContent = String(usage);

  if (usage > THRESHOLD) {
    estimate.textContent = 'Please contact our sales team';
    estimate.classList.add('is-contact');
  } else {
    var won = BASE + usage * RATE;
    estimate.textContent = 'Estimated fee ' + won.toLocaleString('ko-KR') + 'KRW';
    estimate.classList.remove('is-contact');
  }
}

08Partner logo bar — a row that recomputes how many logos fit the current width

Why the logo count is recomputed from the measured width, not fixed at a set number

Floor-dividing the current width by one logo's measured width plus the gap is what tells item 08 how many logos fit one row, and anything past that count gets display:none until the same division reruns on the next width change.

Partner logo bar — a row that recomputes how many logos fit the current width
Open in a new tab

08 · Partner logo bar — a row that recomputes how many logos fit the current width — Measured area 8.32% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

querySelectorAlloffsetWidthtext-overflow

What the numbers say

Cumulative area is 8.32% (max frame 5.43%) but mean intensity is the highest of the nine at 194.6, over 9 of 23 frames, because several logos appear or vanish at once instead of fading gradually.

function apply(width) {
  viewport.style.width = width + 'px';
  var itemWidth = items[0].offsetWidth || 80;
  var fitCount = Math.max(1, Math.floor((width + GAP) / (itemWidth + GAP)));
  fitCount = Math.min(fitCount, items.length);
  items.forEach(function (item, i) {
    item.style.display = i < fitCount ? '' : 'none';
  });
  chip.textContent = fitCount + 'shown';
}

09Pricing footer — a recommender that works out the matching plan from typed conditions

Why the plan recommender checks limits in order instead of highlighting a fixed column

Checking a typed condition like headcount and storage against a limits array in order and picking the first plan whose limits satisfy both is what decides which single column item 09 highlights with a border and a scale, so a condition beyond every plan's limits still lands on the closest match instead of a column chosen in advance.

Pricing footer — a recommender that works out the matching plan from typed conditions
Open in a new tab

09 · Pricing footer — a recommender that works out the matching plan from typed conditions — Measured area 14.082% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

querySelectorAllgrid-template-columnsscale(1.05)

What the numbers say

Cumulative area is 14.08% (max frame 9.68%, mean intensity 89.1) over 7 of 23 frames as four typed conditions cycle through the same sequential check.

function findMatch(need) {
  for (var i = 0; i < LIMITS.length; i++) {
    if (LIMITS[i].seats >= need.headcount && LIMITS[i].storageGb >= need.storage) return LIMITS[i].name;
  }
  return LIMITS[LIMITS.length - 1].name;
}

function apply(need) {
  headcountEl.textContent = String(need.headcount);
  storageEl.textContent = String(need.storage);
  var match = findMatch(need);
  planEls.forEach(function (el) {
    el.classList.toggle('is-match', el.getAttribute('data-name') === match);
  });
  chip.textContent = 'Recommended plan ' + match;
}

Where this actually gets used — a team rebuilding a pricing page

A team rebuilding a subscription pricing page usually gets stuck first on something like 01 — the moment a visitor flips the annual/monthly switch, every price on the page has to recompute instead of swapping between two hardcoded numbers that drift out of sync the next time a plan gets added.

Marketing teams running a discount campaign reach for something like 04's discount badge, which works out its own percentage from list price and sale price instead of having someone type a rounded number that goes stale the moment the promotion changes.

Usage-based B2B pricing teams lean on something like 07's enterprise CTA, which turns typed usage into a live estimate on the spot — instead of a rep calculating a quote by hand for every visitor who asks.

When a negative discount or a runaway digit count breaks the badge math

A computed layer only pays off if the edge cases feeding it are handled on purpose, not by accident.

Why a sale price above list price would print a negative discount percentage

Item 04's percentage formula never checks that the sale price is actually lower than the list price, so typing a sale price above the list price would divide a negative gap by the list price and print a negative number on the badge instead of hiding it.

Why an unrounded yearly-equivalent price could run digits past the card width

Item 01 always wraps its recomputed price in Math.round before animateValue ever touches it, because animating a raw floating-point value across several frames would otherwise leave stray decimal digits flickering inside a card sized for a short integer.

Why an empty reviews array would divide by zero for the headline average

reduce over an empty review array still returns 0, but dividing that 0 by a review count of 0 in item 06's average function is what would actually produce NaN on the headline number — the full guard against that case, not just this summary, sits behind gdcg4wsb in the zip linked from this post.

Three variants, and what reduced-motion turns off here

Variant What changes What it trades off
Discount badge urgency threshold Item 04's URGENT_RATIO from .25 to .5 .25 keeps the badge calm until the very end so the flip to urgent color reads as a real signal; .5 turns it urgent before half the time is gone, so the badge feels permanently urgent
Enterprise contact threshold Item 07's THRESHOLD from 1000 to 500 1000 leaves more of the slider showing a live estimate a visitor can explore themselves; 500 routes bigger customers to a human sooner but turns half the slider into a dead zone with no price shown
Comparison table highlight blend Item 02's --diff-alpha base .12 and range .35 narrowed to .06 and .20 .12-to-.47 makes even a single differing row easy to spot but can make a five-row difference look like the whole table is stained; .06-to-.26 stays calmer but a lone differing row barely stands out

What reduced motion turns off, item by item

prefers-reduced-motion: reduce removes travel, not computation, across all nine: item 01's rolling digits jump straight to the eased result while the discount math underneath still runs in full, and item 05's accordion still measures and sums scrollHeight before snapping open instead of easing into place. Item 04's countdown keeps recomputing its remaining ratio every loop even though the interval driving the loop itself is what a reduced-motion build would slow down first.

Where numbers reshape layout — discount rate, usage, review scores

None of these nine pricing sections has a fixed pixel layout waiting for a plan to fill it; the numbers behind the plan decide what a stylesheet ends up reading.

Why a five-row diffCount darkens a comparison row further than a one-row diffCount

A single differing row barely nudges --diff-alpha off its base value, while a full five-row difference pushes the same variable toward its ceiling, which is exactly why item 02 treats diffCount as a ratio instead of a boolean.

Why crossing the enterprise threshold swaps the copy instead of just disabling the estimate

A usage figure under THRESHOLD keeps a running estimate line updated, but crossing 1000 units swaps that same line over to a sales-contact prompt because a live number stops being useful advice once the underlying rate no longer applies at that scale.

Why the testimonial card height follows the longest review, not a fixed line count

Remeasuring all three cards' scrollHeight one frame after the review text swaps and setting the largest value as every card's height is what keeps a short review from sitting inside an oversized box built for a longer one.

Questions worth checking before you ship a pricing page

Are these pricing page design examples free to reuse commercially?

Yes — nothing in this set loads a third-party library, a UI framework, or an external CDN script, so dropping any of the nine into a commercial project carries no license to track down or credit to add.

Why does the comparison table sometimes barely change color between two plans?

Only because diffCount stayed low for that comparison — a single differing row keeps --diff-alpha near its base value on purpose, and swapping in a scenario with more real differences is what raises it, not a bug in the highlight math.

Is this the same as a pricing page design codepen someone pastes together?

Not quite — there's no CodePen account or embed here, just a plain index.html file inside the zip that runs the same computed layer described above the moment it's opened in a browser.

Sources: MDN — Auto-placement in grid layout · MDN — the tab role · MDN — prefers-reduced-motion · WAI-ARIA APG — Accordion Pattern

The same diff-count idea behind item 02 shows up again in a pricing table where nine small hover and reveal animations sit on top of one existing table, and the billing-switch computation item 03 leans on returns in a different shape inside a plan set where the interactions themselves, not the sections around them, are what gets swapped nine ways — one post recomputes what a whole section shows, the other recomputes how a single existing table behaves.

Enter the archive password

The password is inside this article. You will find it as you read.