Pricing — Monthly/annual toggle
A self-contained three-tier pricing page for the fictional Northwind Cloud, art-directed in a neutral product-UI palette with Inter type and soft shadows. Starter, Pro and Scale cards sit under a pill-shaped Monthly versus Annual segmented toggle that carries a Save 20 percent badge. Choosing a period slides the active pill, count-up animates every headline price between figures, and the per-seat billing note swaps accordingly. The choice persists via aria-pressed and localStorage, the toggle is fully keyboard operable, and a small toast confirms each switch.
MCP
Code
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-lg: 0 8px 24px rgba(16, 19, 34, 0.08);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ---------- Layout ---------- */
.page {
max-width: 1040px;
margin: 0 auto;
padding: clamp(40px, 7vw, 88px) 20px 64px;
}
/* ---------- Hero ---------- */
.hero {
text-align: center;
max-width: 640px;
margin: 0 auto;
}
.hero__eyebrow {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border-radius: 999px;
background: var(--brand-50);
color: var(--brand-700);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
}
.hero__title {
margin: 18px 0 12px;
font-size: clamp(28px, 5vw, 44px);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.1;
}
.hero__sub {
margin: 0 auto;
max-width: 520px;
color: var(--muted);
font-size: 16px;
}
/* ---------- Billing toggle ---------- */
.billing {
margin-top: 32px;
}
.seg {
position: relative;
display: inline-flex;
padding: 4px;
border-radius: 999px;
background: var(--white);
border: 1px solid var(--line);
box-shadow: var(--sh-sm);
}
.seg__pill {
position: absolute;
top: 4px;
left: 4px;
height: calc(100% - 8px);
border-radius: 999px;
background: var(--ink);
box-shadow: var(--sh-sm);
transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1),
width 0.32s cubic-bezier(0.22, 1, 0.36, 1);
z-index: 0;
}
.seg__btn {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
padding: 9px 20px;
border-radius: 999px;
font-size: 14px;
font-weight: 600;
color: var(--ink-2);
transition: color 0.2s ease;
white-space: nowrap;
}
.seg__btn.is-active {
color: var(--white);
}
.seg__save {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
background: var(--accent-soft);
color: #057a70;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.01em;
transition: background 0.2s ease, color 0.2s ease;
}
.seg__btn.is-active .seg__save {
background: rgba(255, 255, 255, 0.18);
color: #b5f3ec;
}
.billing__note {
margin: 14px 0 0;
font-size: 13px;
color: var(--muted);
min-height: 18px;
}
/* ---------- Plans grid ---------- */
.plans {
margin-top: 44px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
align-items: stretch;
}
.plan {
position: relative;
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 26px 24px;
box-shadow: var(--sh-sm);
transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease;
}
.plan:hover {
transform: translateY(-4px);
box-shadow: var(--sh-lg);
border-color: var(--line-2);
}
.plan--popular {
border-color: var(--brand);
box-shadow: 0 12px 32px rgba(91, 91, 240, 0.18);
}
.plan--popular:hover {
box-shadow: 0 16px 40px rgba(91, 91, 240, 0.24);
}
.plan__ribbon {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
padding: 5px 14px;
border-radius: 999px;
background: var(--brand);
color: var(--white);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
box-shadow: 0 4px 12px rgba(91, 91, 240, 0.35);
white-space: nowrap;
}
.plan__head {
margin-bottom: 18px;
}
.plan__name {
margin: 0 0 4px;
font-size: 18px;
font-weight: 700;
}
.plan__tag {
margin: 0;
font-size: 13.5px;
color: var(--muted);
}
/* ---------- Price ---------- */
.plan__price {
display: flex;
align-items: baseline;
gap: 2px;
}
.plan__currency {
font-size: 22px;
font-weight: 600;
color: var(--ink-2);
align-self: flex-start;
margin-top: 6px;
}
.plan__amount {
font-size: clamp(40px, 7vw, 52px);
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.plan__amount.is-swapping {
animation: priceSwap 0.36s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes priceSwap {
0% {
transform: translateY(0);
opacity: 1;
}
45% {
transform: translateY(-8px);
opacity: 0.35;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.plan__per {
font-size: 15px;
font-weight: 600;
color: var(--muted);
align-self: flex-end;
margin-bottom: 7px;
margin-left: 2px;
}
.plan__billed {
margin: 6px 0 20px;
font-size: 13px;
color: var(--muted);
min-height: 18px;
}
/* ---------- Buttons ---------- */
.btn {
width: 100%;
padding: 12px 18px;
border-radius: var(--r-md);
border: 1px solid transparent;
font-size: 14.5px;
font-weight: 600;
transition: transform 0.12s ease, background 0.18s ease, box-shadow 0.18s ease,
border-color 0.18s ease, color 0.18s ease;
}
.btn:active {
transform: scale(0.98);
}
.btn--primary {
background: var(--brand);
color: var(--white);
box-shadow: 0 6px 16px rgba(91, 91, 240, 0.3);
}
.btn--primary:hover {
background: var(--brand-d);
}
.btn--ghost {
background: var(--white);
color: var(--ink);
border-color: var(--line-2);
}
.btn--ghost:hover {
border-color: var(--brand);
color: var(--brand-700);
background: var(--brand-50);
}
/* ---------- Features ---------- */
.plan__feats {
list-style: none;
margin: 24px 0 0;
padding: 22px 0 0;
border-top: 1px solid var(--line);
display: grid;
gap: 12px;
}
.plan__feats li {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 14px;
color: var(--ink-2);
}
.ic-check {
flex: none;
width: 18px;
height: 18px;
margin-top: 1px;
fill: none;
stroke: var(--accent);
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
}
.plan--popular .ic-check {
stroke: var(--brand);
}
/* ---------- Footnote ---------- */
.footnote {
margin: 36px auto 0;
max-width: 560px;
text-align: center;
font-size: 13px;
color: var(--muted);
}
#period-summary {
display: block;
margin-top: 4px;
font-weight: 600;
color: var(--ink-2);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 16px);
background: var(--ink);
color: var(--white);
padding: 12px 18px;
border-radius: var(--r-md);
font-size: 14px;
font-weight: 500;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.24s ease, transform 0.24s ease;
z-index: 50;
max-width: calc(100vw - 32px);
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
@media (prefers-reduced-motion: reduce) {
.seg__pill,
.plan,
.btn,
.toast {
transition: none;
}
.plan__amount.is-swapping {
animation: none;
}
}
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.plans {
grid-template-columns: 1fr;
max-width: 460px;
margin-left: auto;
margin-right: auto;
}
.plan--popular {
order: -1;
}
}
@media (max-width: 520px) {
.page {
padding-top: 36px;
}
.seg {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
}
.seg__btn {
justify-content: center;
padding: 10px 8px;
}
.seg__save {
font-size: 10px;
padding: 2px 6px;
}
.plan {
padding: 22px 18px;
}
.plan__amount {
font-size: 42px;
}
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.hidden = false;
// force reflow so the transition replays
void toastEl.offsetWidth;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
setTimeout(function () {
toastEl.hidden = true;
}, 260);
}, 2400);
}
/* ---------- Elements ---------- */
var seg = document.getElementById("billing-seg");
var pill = document.getElementById("seg-pill");
var btnMonthly = document.getElementById("btn-monthly");
var btnAnnual = document.getElementById("btn-annual");
var billingNote = document.getElementById("billing-note");
var periodSummary = document.getElementById("period-summary");
var buttons = [btnMonthly, btnAnnual];
var amounts = Array.prototype.slice.call(
document.querySelectorAll(".plan__amount")
);
var perLabels = Array.prototype.slice.call(
document.querySelectorAll("[data-period-label]")
);
var billedNotes = Array.prototype.slice.call(
document.querySelectorAll("[data-billed-note]")
);
var prefersReduced =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var current = "monthly";
/* ---------- Move the sliding pill under the active button ---------- */
function movePill(btn) {
if (!pill || !btn) return;
// btn.offsetLeft is measured from the seg's padding box (4px inset),
// which is also where the pill's left:4px origin sits.
pill.style.width = btn.offsetWidth + "px";
pill.style.transform = "translateX(" + (btn.offsetLeft - 4) + "px)";
}
/* ---------- Animated number count-up between values ---------- */
function countTo(el, from, to) {
if (from === to) {
el.textContent = String(to);
return;
}
if (prefersReduced) {
el.textContent = String(to);
return;
}
el.classList.remove("is-swapping");
void el.offsetWidth;
el.classList.add("is-swapping");
var duration = 420;
var start = null;
function ease(t) {
return 1 - Math.pow(1 - t, 3);
}
function step(ts) {
if (start === null) start = ts;
var p = Math.min((ts - start) / duration, 1);
var val = Math.round(from + (to - from) * ease(p));
el.textContent = String(val);
if (p < 1) {
requestAnimationFrame(step);
} else {
el.textContent = String(to);
}
}
requestAnimationFrame(step);
}
/* ---------- Apply a billing period ---------- */
function setPeriod(period, opts) {
opts = opts || {};
var isAnnual = period === "annual";
current = period;
// Toggle button state + aria-pressed (persists the choice)
buttons.forEach(function (b) {
var active = b.dataset.period === period;
b.classList.toggle("is-active", active);
b.setAttribute("aria-pressed", active ? "true" : "false");
});
var activeBtn = isAnnual ? btnAnnual : btnMonthly;
movePill(activeBtn);
// Prices
amounts.forEach(function (el) {
var from = parseInt(el.textContent, 10) || 0;
var to = parseInt(
isAnnual ? el.dataset.annual : el.dataset.monthly,
10
);
if (opts.instant) {
el.textContent = String(to);
} else {
countTo(el, from, to);
}
});
// Per-period labels
perLabels.forEach(function (el) {
el.textContent = isAnnual ? "/mo" : "/mo";
});
billedNotes.forEach(function (el) {
el.textContent = isAnnual
? "per seat, billed annually"
: "per seat, billed monthly";
});
// Notes / summary
billingNote.textContent = isAnnual
? "Billed once a year — that's 2 months free. Cancel anytime."
: "Billed monthly. Cancel anytime.";
periodSummary.textContent = isAnnual
? "You're viewing annual pricing (20% off)."
: "You're viewing monthly pricing.";
// Persist
try {
localStorage.setItem("nw-billing", period);
} catch (e) {
/* storage may be blocked in sandboxed iframes */
}
}
/* ---------- Events ---------- */
buttons.forEach(function (b) {
b.addEventListener("click", function () {
if (current === b.dataset.period) return;
setPeriod(b.dataset.period);
if (b.dataset.period === "annual") {
toast("Annual billing applied — you save 20%.");
} else {
toast("Switched to monthly billing.");
}
});
});
// Keyboard: arrow keys move between the two segments
seg.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
e.preventDefault();
btnAnnual.focus();
btnAnnual.click();
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
e.preventDefault();
btnMonthly.focus();
btnMonthly.click();
} else if (e.key === "Home") {
e.preventDefault();
btnMonthly.focus();
btnMonthly.click();
} else if (e.key === "End") {
e.preventDefault();
btnAnnual.focus();
btnAnnual.click();
}
});
// Plan CTAs
document.querySelectorAll("[data-plan-cta]").forEach(function (btn) {
btn.addEventListener("click", function () {
var plan = btn.getAttribute("data-plan-cta");
var period = current === "annual" ? "annual" : "monthly";
if (plan === "Scale") {
toast("Opening sales chat for the Scale plan…");
} else {
toast("Starting your " + plan + " trial (" + period + " billing).");
}
});
});
/* ---------- Init: restore saved choice, position pill ---------- */
var saved = "monthly";
try {
var s = localStorage.getItem("nw-billing");
if (s === "annual" || s === "monthly") saved = s;
} catch (e) {
/* ignore */
}
// Position pill instantly first, then apply period without animating the
// count-up on initial paint.
requestAnimationFrame(function () {
setPeriod(saved, { instant: true });
});
// Keep the pill aligned on resize.
var resizeTimer = null;
window.addEventListener("resize", function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
movePill(current === "annual" ? btnAnnual : btnMonthly);
}, 80);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind — Pricing (Monthly / Annual toggle)</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page" role="main">
<section class="hero">
<span class="hero__eyebrow">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<path d="M13 2 4 14h6l-1 8 9-12h-6l1-8z" fill="currentColor" />
</svg>
Northwind Cloud
</span>
<h1 class="hero__title">Pricing that scales with your team</h1>
<p class="hero__sub">
Start free, upgrade when you ship. Switch to annual billing and keep two months
on us — every plan, every seat.
</p>
<!-- Monthly / Annual segmented toggle -->
<div class="billing" role="group" aria-label="Billing period">
<div class="seg" id="billing-seg">
<span class="seg__pill" aria-hidden="true" id="seg-pill"></span>
<button
type="button"
class="seg__btn is-active"
id="btn-monthly"
aria-pressed="true"
data-period="monthly"
>
Monthly
</button>
<button
type="button"
class="seg__btn"
id="btn-annual"
aria-pressed="false"
data-period="annual"
>
Annual
<span class="seg__save">Save 20%</span>
</button>
</div>
<p class="billing__note" aria-live="polite" id="billing-note">
Billed monthly. Cancel anytime.
</p>
</div>
</section>
<!-- Plans -->
<section class="plans" aria-label="Subscription plans">
<!-- Starter -->
<article class="plan" data-plan="starter">
<header class="plan__head">
<h2 class="plan__name">Starter</h2>
<p class="plan__tag">For solo builders and side projects.</p>
</header>
<div class="plan__price">
<span class="plan__currency">$</span>
<span
class="plan__amount"
data-monthly="12"
data-annual="10"
aria-live="polite"
>12</span>
<span class="plan__per" data-period-label>/mo</span>
</div>
<p class="plan__billed" data-billed-note>per seat, billed monthly</p>
<button type="button" class="btn btn--ghost" data-plan-cta="Starter">
Start free trial
</button>
<ul class="plan__feats">
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> 3 projects</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> 5 GB storage</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> Community support</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> Weekly usage digest</li>
</ul>
</article>
<!-- Pro (most popular) -->
<article class="plan plan--popular" data-plan="pro">
<span class="plan__ribbon">Most popular</span>
<header class="plan__head">
<h2 class="plan__name">Pro</h2>
<p class="plan__tag">For growing product teams.</p>
</header>
<div class="plan__price">
<span class="plan__currency">$</span>
<span
class="plan__amount"
data-monthly="29"
data-annual="23"
aria-live="polite"
>29</span>
<span class="plan__per" data-period-label>/mo</span>
</div>
<p class="plan__billed" data-billed-note>per seat, billed monthly</p>
<button type="button" class="btn btn--primary" data-plan-cta="Pro">
Start free trial
</button>
<ul class="plan__feats">
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> Unlimited projects</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> 100 GB storage</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> Priority email support</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> Advanced analytics</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> 5 team integrations</li>
</ul>
</article>
<!-- Scale -->
<article class="plan" data-plan="scale">
<header class="plan__head">
<h2 class="plan__name">Scale</h2>
<p class="plan__tag">For orgs with serious throughput.</p>
</header>
<div class="plan__price">
<span class="plan__currency">$</span>
<span
class="plan__amount"
data-monthly="79"
data-annual="63"
aria-live="polite"
>79</span>
<span class="plan__per" data-period-label>/mo</span>
</div>
<p class="plan__billed" data-billed-note>per seat, billed monthly</p>
<button type="button" class="btn btn--ghost" data-plan-cta="Scale">
Talk to sales
</button>
<ul class="plan__feats">
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> Everything in Pro</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> 1 TB storage</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> SSO & SCIM</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> 99.95% uptime SLA</li>
<li><svg class="ic-check" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10 4 4 8-9" /></svg> Dedicated success manager</li>
</ul>
</article>
</section>
<p class="footnote">
Prices in USD. Annual plans are billed once per year at the discounted rate shown.
<span id="period-summary" aria-live="polite"></span>
</p>
</main>
<!-- Toast -->
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>Monthly/annual toggle
A complete three-tier pricing page for Northwind Cloud, a fictional SaaS, built in the neutral product-UI palette with Inter type, hairline borders and soft shadows. A centered hero introduces the plans and hosts a pill-shaped Monthly | Annual segmented control; the Annual option wears a teal Save 20% badge. Below it sit three cards — Starter, Pro and Scale — each with a tagline, a large tabular-numeral price, a per-period label, a per-seat billing note, a CTA and a checkmarked feature list. The Pro card is lifted as the Most popular tier with a brand-coloured ribbon and a deeper glow.
Toggling the control is the core interaction. The active segment’s sliding pill animates into place,
every headline price count-ups between its monthly and annual figure (with a subtle vertical swap), and
the billing note flips between “billed monthly” and “billed annually”. The chosen period is reflected in
each button’s aria-pressed state and persisted to localStorage, so the page reopens on the reader’s
last choice. A toast confirms every switch, and the plan CTAs raise their own toast noting the selected
plan and billing period.
The toggle is keyboard operable — Arrow, Home and End keys move between the two segments — and the whole
page is responsive down to ~360px, where the segmented control stretches full width and the cards stack
with Pro floated to the top. Focus-visible rings, hover and active states, and a prefers-reduced-motion
guard round it out. Vanilla JS only, no frameworks, no build step, and no network requests beyond the
single Google Fonts link.
Illustrative UI only — Northwind Cloud, its plans and prices are fictional.