Pricing
Module bundle builder
Tick the parts you need and watch the total settle
Tick the parts you need and watch the total settle
Build a modular pricing configurator with Tailwind CSS. Use a max-w-4xl two-column grid. The left column is a list of six selectable module cards: each a bordered rounded-xl row with a checkbox on the left, a module name and a one-line description, and a price on the right; the core module is pre-selected and disabled with a muted "Required" label. Selected cards gain a neutral-900 border and a faint background. The right column is a sticky summary card listing the chosen modules as rows with names and prices, a bundle discount line in green that appears from three modules, a bold total, a dark checkout button and a muted note about monthly billing. Then add JavaScript that toggles modules, rebuilds the summary rows, applies a ten percent discount at three modules and fifteen at five, and updates the total and the button label with the module count. <section class="mx-auto max-w-4xl px-6 py-14">
<div class="mb-8 max-w-xl">
<h2 class="text-3xl font-semibold tracking-tight text-neutral-900">Build the plan you actually need</h2>
<p class="mt-3 text-neutral-500">Start with the core platform and add only the modules your team will use.</p>
</div>
<div class="grid gap-6 lg:grid-cols-[1fr_320px]">
<div id="moduleList" class="space-y-2.5">
<label class="module flex cursor-not-allowed items-center gap-4 rounded-xl border border-neutral-900 bg-neutral-50 p-4">
<input type="checkbox" checked disabled data-name="Core platform" data-price="49" class="rounded border-neutral-300" />
<span class="min-w-0 flex-1">
<span class="flex items-center gap-2 text-sm font-medium text-neutral-900">Core platform <span class="rounded-full bg-neutral-200 px-2 py-0.5 text-[10px] text-neutral-600">Required</span></span>
<span class="block text-xs text-neutral-500">Workspaces, roles, audit log and the API.</span>
</span>
<span class="text-sm font-medium text-neutral-900">$49</span>
</label>
<label class="module flex cursor-pointer items-center gap-4 rounded-xl border border-neutral-200 p-4 transition hover:bg-neutral-50">
<input type="checkbox" data-name="Reporting" data-price="29" class="rounded border-neutral-300" />
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Reporting</span>
<span class="block text-xs text-neutral-500">Dashboards, scheduled exports and shared views.</span>
</span>
<span class="text-sm font-medium text-neutral-900">$29</span>
</label>
<label class="module flex cursor-pointer items-center gap-4 rounded-xl border border-neutral-200 p-4 transition hover:bg-neutral-50">
<input type="checkbox" data-name="Automations" data-price="39" class="rounded border-neutral-300" />
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Automations</span>
<span class="block text-xs text-neutral-500">Triggers, branching workflows and webhook replay.</span>
</span>
<span class="text-sm font-medium text-neutral-900">$39</span>
</label>
<label class="module flex cursor-pointer items-center gap-4 rounded-xl border border-neutral-200 p-4 transition hover:bg-neutral-50">
<input type="checkbox" data-name="Compliance pack" data-price="59" class="rounded border-neutral-300" />
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Compliance pack</span>
<span class="block text-xs text-neutral-500">Retention policies, legal hold and evidence export.</span>
</span>
<span class="text-sm font-medium text-neutral-900">$59</span>
</label>
<label class="module flex cursor-pointer items-center gap-4 rounded-xl border border-neutral-200 p-4 transition hover:bg-neutral-50">
<input type="checkbox" data-name="Data residency" data-price="35" class="rounded border-neutral-300" />
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Data residency</span>
<span class="block text-xs text-neutral-500">Pin storage and processing to a chosen region.</span>
</span>
<span class="text-sm font-medium text-neutral-900">$35</span>
</label>
<label class="module flex cursor-pointer items-center gap-4 rounded-xl border border-neutral-200 p-4 transition hover:bg-neutral-50">
<input type="checkbox" data-name="Premium support" data-price="99" class="rounded border-neutral-300" />
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Premium support</span>
<span class="block text-xs text-neutral-500">One-hour response, named engineer, quarterly review.</span>
</span>
<span class="text-sm font-medium text-neutral-900">$99</span>
</label>
</div>
<aside class="lg:sticky lg:top-24 lg:self-start">
<div class="rounded-2xl border border-neutral-200 bg-white p-6">
<p class="text-xs font-semibold uppercase tracking-widest text-neutral-400">Your bundle</p>
<div id="bundleRows" class="mt-4 space-y-2 text-sm"></div>
<div id="bundleDiscount" class="mt-2 hidden items-center justify-between text-sm text-green-700">
<span id="bundleDiscountLabel">Bundle discount</span>
<span id="bundleDiscountValue" class="font-mono">−$0</span>
</div>
<div class="mt-3 flex items-center justify-between border-t border-neutral-200 pt-3 text-base font-semibold text-neutral-900">
<span>Total</span>
<span id="bundleTotal" class="font-mono">$49</span>
</div>
<button id="bundleCta" class="mt-5 w-full rounded-xl bg-neutral-900 py-3 text-sm font-medium text-white transition hover:bg-neutral-800">Continue with 1 module</button>
<p class="mt-3 text-xs leading-relaxed text-neutral-400">Billed monthly. Add or drop modules whenever you like — we bill the difference pro rata.</p>
</div>
</aside>
</div>
</section> var boxes = Array.prototype.slice.call(document.querySelectorAll(".module input"));
var rows = document.getElementById("bundleRows");
var discountRow = document.getElementById("bundleDiscount");
var discountLabel = document.getElementById("bundleDiscountLabel");
var discountValue = document.getElementById("bundleDiscountValue");
var totalEl = document.getElementById("bundleTotal");
var cta = document.getElementById("bundleCta");
function money(n) {
return "$" + n.toFixed(n % 1 === 0 ? 0 : 2);
}
function rate(count) {
if (count >= 5) return 0.15;
if (count >= 3) return 0.1;
return 0;
}
function sync() {
var chosen = boxes.filter(function (b) {
return b.checked;
});
rows.innerHTML = chosen
.map(function (b) {
return (
'<div class="flex items-center justify-between text-neutral-600">' +
"<span>" + b.dataset.name + "</span>" +
'<span class="font-mono text-neutral-900">' + money(Number(b.dataset.price)) + "</span>" +
"</div>"
);
})
.join("");
var subtotal = chosen.reduce(function (sum, b) {
return sum + Number(b.dataset.price);
}, 0);
var off = subtotal * rate(chosen.length);
discountRow.classList.toggle("hidden", off === 0);
discountRow.classList.toggle("flex", off > 0);
discountLabel.textContent = "Bundle discount (" + Math.round(rate(chosen.length) * 100) + "%)";
discountValue.textContent = "−" + money(off);
totalEl.textContent = money(subtotal - off);
cta.textContent = "Continue with " + chosen.length + (chosen.length === 1 ? " module" : " modules");
boxes.forEach(function (b) {
var card = b.closest(".module");
card.classList.toggle("border-neutral-900", b.checked);
card.classList.toggle("bg-neutral-50", b.checked);
card.classList.toggle("border-neutral-200", !b.checked);
});
}
boxes.forEach(function (b) {
b.addEventListener("change", sync);
});
sync();