Dental — Cost Estimator
A trust-first dental treatment cost estimator where patients tick the procedures they need from priced cards, drag an insurance coverage slider, and watch a live summary tally the subtotal, insurance contribution, and out-of-pocket balance with smooth count-up animations and clear savings badges.
MCP
Code
:root {
--blue: #2563eb;
--blue-d: #1d4ed8;
--blue-50: #eff5ff;
--mint: #34d399;
--mint-50: #e8fbf3;
--ink: #0f2740;
--ink-2: #3a5169;
--muted: #6b7c90;
--bg: #f3f8ff;
--white: #fff;
--line: rgba(15, 39, 64, 0.10);
--line-2: rgba(15, 39, 64, 0.18);
--ok: #22b07d;
--warn: #d98a2b;
--danger: #e05252;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--shadow-sm: 0 1px 2px rgba(15, 39, 64, 0.06);
--shadow-md: 0 10px 30px -12px rgba(15, 39, 64, 0.22);
--shadow-lg: 0 24px 60px -20px rgba(37, 99, 235, 0.28);
}
* { 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:
radial-gradient(1100px 520px at 80% -10%, var(--blue-50), transparent 60%),
radial-gradient(900px 500px at -10% 110%, var(--mint-50), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 32px 20px 56px;
}
/* Masthead */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 13px;
color: var(--white);
background: linear-gradient(135deg, var(--blue), var(--blue-d));
box-shadow: var(--shadow-md);
}
.brand-txt { display: flex; flex-direction: column; line-height: 1.2; }
.brand-txt strong { font-size: 1.06rem; font-weight: 800; letter-spacing: -0.01em; }
.brand-txt span { font-size: 0.82rem; color: var(--muted); }
.pill {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 7px 13px;
font-size: 0.78rem;
font-weight: 600;
color: var(--blue-d);
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--shadow-sm);
}
.pill .dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--mint);
box-shadow: 0 0 0 4px var(--mint-50);
}
/* Layout */
.grid {
display: grid;
grid-template-columns: 1fr 360px;
gap: 22px;
align-items: start;
}
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-md);
}
/* Picker */
.picker { display: flex; flex-direction: column; gap: 18px; }
.group { padding: 20px 20px 8px; }
.group-head h2 {
margin: 0;
font-size: 1.02rem;
font-weight: 800;
letter-spacing: -0.01em;
}
.group-head p {
margin: 3px 0 12px;
font-size: 0.85rem;
color: var(--muted);
}
.rows { list-style: none; margin: 0; padding: 0; }
.rows li { margin: 0 -20px; }
.rows li + li .row { border-top: 1px solid var(--line); }
.row {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 20px;
cursor: pointer;
position: relative;
transition: background 0.16s ease;
}
.row:hover { background: var(--blue-50); }
.row input {
position: absolute;
opacity: 0;
width: 0; height: 0;
}
.check {
flex: none;
width: 24px;
height: 24px;
border-radius: 8px;
border: 2px solid var(--line-2);
background: var(--white);
display: grid;
place-items: center;
transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
}
.check::after {
content: "";
width: 12px;
height: 7px;
border-left: 2.4px solid var(--white);
border-bottom: 2.4px solid var(--white);
transform: rotate(-45deg) scale(0.4);
opacity: 0;
margin-top: -2px;
transition: opacity 0.14s ease, transform 0.16s ease;
}
.row-body { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.row-name { font-weight: 600; font-size: 0.94rem; }
.row-note { font-size: 0.8rem; color: var(--muted); }
.row-price {
flex: none;
font-weight: 700;
font-size: 0.94rem;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
/* checked state */
.row input:checked ~ .check {
border-color: var(--blue);
background: var(--blue);
}
.row input:checked ~ .check::after {
opacity: 1;
transform: rotate(-45deg) scale(1);
}
.row.is-on { background: var(--blue-50); }
.row.is-on .row-price { color: var(--blue-d); }
/* focus */
.row input:focus-visible ~ .check {
outline: 3px solid rgba(37, 99, 235, 0.4);
outline-offset: 2px;
}
/* Summary */
.summary { position: sticky; top: 22px; }
.summary-card { padding: 22px; }
.summary-title {
margin: 0 0 2px;
font-size: 1.1rem;
font-weight: 800;
letter-spacing: -0.01em;
}
.count-line { margin-bottom: 18px; }
.count-label { font-size: 0.84rem; color: var(--muted); font-weight: 500; }
/* Slider */
.slider-block {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 15px 12px;
margin-bottom: 18px;
}
.slider-top {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 10px;
}
.slider-top label { font-size: 0.86rem; font-weight: 600; }
.slider-top output {
font-size: 1.02rem;
font-weight: 800;
color: var(--blue-d);
font-variant-numeric: tabular-nums;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 8px;
border-radius: 999px;
background: linear-gradient(90deg, var(--blue) 0 60%, var(--line) 60% 100%);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--white);
border: 3px solid var(--blue);
box-shadow: var(--shadow-md);
transition: transform 0.12s ease;
}
input[type="range"]::-moz-range-thumb {
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--white);
border: 3px solid var(--blue);
box-shadow: var(--shadow-md);
cursor: pointer;
}
input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.12); }
input[type="range"]:focus-visible {
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
}
.slider-hint {
margin: 9px 0 0;
font-size: 0.78rem;
color: var(--muted);
}
/* Totals */
.totals { margin: 0 0 16px; }
.total-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed var(--line);
}
.total-row dt { margin: 0; font-size: 0.9rem; font-weight: 500; }
.total-row dd {
margin: 0;
font-weight: 700;
font-variant-numeric: tabular-nums;
font-size: 0.98rem;
}
.total-row.muted dt, .total-row.muted dd { color: var(--ok); }
.total-row.grand {
border-bottom: none;
padding-top: 14px;
}
.total-row.grand dt { font-weight: 700; font-size: 1rem; }
.total-row.grand dd {
font-size: 1.5rem;
font-weight: 800;
color: var(--blue-d);
letter-spacing: -0.02em;
}
.badge {
display: inline-block;
margin-left: 6px;
padding: 2px 8px;
font-size: 0.72rem;
font-weight: 700;
color: var(--ok);
background: var(--mint-50);
border-radius: 999px;
vertical-align: middle;
}
/* Split bar */
.bar {
height: 10px;
border-radius: 999px;
background: var(--blue-50);
overflow: hidden;
margin-bottom: 8px;
}
.bar-fill {
display: block;
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--mint), var(--ok));
transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.bar-cap { margin: 0 0 18px; font-size: 0.78rem; color: var(--muted); }
/* Actions */
.actions { display: flex; gap: 10px; }
.btn {
flex: 1;
padding: 12px 14px;
font: inherit;
font-weight: 700;
font-size: 0.9rem;
border-radius: var(--r-md);
border: 1px solid transparent;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.16s ease, background 0.16s ease, border-color 0.16s ease;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid rgba(37, 99, 235, 0.4); outline-offset: 2px; }
.btn.primary {
color: var(--white);
background: linear-gradient(135deg, var(--blue), var(--blue-d));
box-shadow: var(--shadow-md);
}
.btn.primary:hover { box-shadow: var(--shadow-lg); }
.btn.ghost {
color: var(--ink-2);
background: var(--white);
border-color: var(--line-2);
}
.btn.ghost:hover { background: var(--blue-50); border-color: var(--blue); color: var(--blue-d); }
.fineprint {
margin: 14px 0 0;
font-size: 0.72rem;
color: var(--muted);
text-align: center;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 20px);
background: var(--ink);
color: var(--white);
padding: 12px 18px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 520px) {
.wrap { padding: 22px 14px 44px; }
.masthead { flex-wrap: wrap; }
.grid { grid-template-columns: 1fr; }
.summary { position: static; }
.row { gap: 11px; padding: 13px 20px; }
.row-price { font-size: 0.9rem; }
.total-row.grand dd { font-size: 1.35rem; }
}(function () {
"use strict";
var checks = Array.prototype.slice.call(document.querySelectorAll(".tx"));
var coverage = document.getElementById("coverage");
var coverageOut = document.getElementById("coverageOut");
var coverageHint = document.getElementById("coverageHint");
var subtotalEl = document.getElementById("subtotal");
var coveredEl = document.getElementById("covered");
var oopEl = document.getElementById("oop");
var countLabel = document.getElementById("countLabel");
var saveBadge = document.getElementById("saveBadge");
var barFill = document.getElementById("barFill");
var barCap = document.getElementById("barCap");
var copyBtn = document.getElementById("copyBtn");
var resetBtn = document.getElementById("resetBtn");
var toastEl = document.getElementById("toast");
var fmt = function (n) {
return "$" + Math.round(n).toLocaleString("en-US");
};
var hintFor = function (pct) {
if (pct === 0) return "Paying fully out of pocket";
if (pct < 40) return "Paying most out of pocket";
if (pct < 70) return "Shared cost with your plan";
if (pct < 100) return "Your plan covers the majority";
return "Fully covered by insurance";
};
// Animated count-up for a currency element.
var anims = new WeakMap();
function animateTo(el, target, prefixMinus) {
var start = anims.get(el);
if (typeof start !== "number") start = 0;
var from = start;
var t0 = performance.now();
var dur = 420;
function step(now) {
var p = Math.min(1, (now - t0) / dur);
var eased = 1 - Math.pow(1 - p, 3);
var val = from + (target - from) * eased;
el.textContent = (prefixMinus && target > 0 ? "–" : "") + fmt(val);
if (p < 1) requestAnimationFrame(step);
else {
el.textContent = (prefixMinus && target > 0 ? "–" : "") + fmt(target);
anims.set(el, target);
}
}
anims.set(el, target);
requestAnimationFrame(step);
anims.set(el, target);
// store the animated running value separately
anims.set(el, from);
(function run(now) {
var p = Math.min(1, (now - t0) / dur);
var eased = 1 - Math.pow(1 - p, 3);
var val = from + (target - from) * eased;
el.textContent = (prefixMinus && target > 0 ? "–" : "") + fmt(val);
anims.set(el, val);
if (p < 1) requestAnimationFrame(run);
else { anims.set(el, target); }
})(performance.now());
}
function selected() {
return checks.filter(function (c) { return c.checked; });
}
function update() {
var chosen = selected();
var subtotal = chosen.reduce(function (sum, c) {
return sum + Number(c.getAttribute("data-price"));
}, 0);
var pct = Number(coverage.value);
var covered = subtotal * (pct / 100);
var oop = subtotal - covered;
// slider fill
coverage.style.background =
"linear-gradient(90deg, var(--blue) 0 " + pct + "%, var(--line) " + pct + "% 100%)";
coverageOut.textContent = pct + "%";
coverageHint.textContent = hintFor(pct);
// count label
var n = chosen.length;
countLabel.textContent = n === 0
? "No treatments selected yet"
: n + (n === 1 ? " treatment" : " treatments") + " selected";
// numbers
animateTo(subtotalEl, subtotal, false);
animateTo(coveredEl, covered, true);
animateTo(oopEl, oop, false);
// savings badge
if (covered >= 1) {
saveBadge.hidden = false;
saveBadge.textContent = "–" + fmt(covered);
} else {
saveBadge.hidden = true;
}
// split bar: portion covered by insurance
var coverShare = subtotal > 0 ? (covered / subtotal) * 100 : 0;
barFill.style.width = coverShare + "%";
if (subtotal === 0) {
barCap.textContent = "Select treatments to see your split";
} else if (covered < 1) {
barCap.textContent = "You pay the full " + fmt(subtotal);
} else {
barCap.textContent = "Insurance " + fmt(covered) + " · You pay " + fmt(oop);
}
}
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
// Toggle row highlight class
checks.forEach(function (c) {
c.addEventListener("change", function () {
var row = c.closest(".row");
if (row) row.classList.toggle("is-on", c.checked);
update();
});
});
coverage.addEventListener("input", update);
resetBtn.addEventListener("click", function () {
checks.forEach(function (c) {
c.checked = false;
var row = c.closest(".row");
if (row) row.classList.remove("is-on");
});
coverage.value = 60;
update();
toast("Estimate reset");
});
copyBtn.addEventListener("click", function () {
var chosen = selected();
if (chosen.length === 0) {
toast("Select a treatment first");
return;
}
var subtotal = chosen.reduce(function (s, c) { return s + Number(c.getAttribute("data-price")); }, 0);
var pct = Number(coverage.value);
var covered = subtotal * (pct / 100);
var oop = subtotal - covered;
var lines = ["Brightleaf Dental — estimate", ""];
chosen.forEach(function (c) {
lines.push("• " + c.getAttribute("data-name") + " — " + fmt(Number(c.getAttribute("data-price"))));
});
lines.push("");
lines.push("Subtotal: " + fmt(subtotal));
lines.push("Insurance (" + pct + "%): –" + fmt(covered));
lines.push("Out of pocket: " + fmt(oop));
lines.push("");
lines.push("Illustrative estimate — not a quote.");
var text = lines.join("\n");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function () { toast("Summary copied to clipboard"); },
function () { toast("Copy the summary manually"); }
);
} else {
toast("Clipboard not available");
}
});
// init
update();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dental — Cost Estimator</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="wrap">
<header class="masthead">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3c-2.2 0-3 1.3-4.5 1.3S5 3.4 4 4.5C2.7 5.9 3 8.4 3.6 11c.6 2.6 1 5 1.7 6.6.5 1.1 1 2.4 1.9 2.4 1 0 1.2-1.6 1.5-3 .3-1.4.6-2.6 1.3-2.6s1 1.2 1.3 2.6c.3 1.4.5 3 1.5 3 .9 0 1.4-1.3 1.9-2.4.7-1.6 1.1-4 1.7-6.6.6-2.6.9-5.1-.4-6.5C16.5 3.4 16 4.3 14.5 4.3S14.2 3 12 3Z"/>
</svg>
</span>
<div class="brand-txt">
<strong>Brightleaf Dental</strong>
<span>Treatment cost estimator</span>
</div>
</div>
<span class="pill">
<span class="dot" aria-hidden="true"></span>
Estimate only
</span>
</header>
<div class="grid">
<!-- LEFT: treatment picker -->
<section class="picker" aria-label="Choose treatments">
<div class="card group" data-group="Preventive">
<div class="group-head">
<h2>Preventive</h2>
<p>Routine care to keep things healthy.</p>
</div>
<ul class="rows" role="list">
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="95" data-name="Comprehensive exam" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">Comprehensive exam</span>
<span class="row-note">Full check-up with digital charting</span>
</span>
<span class="row-price">$95</span>
</label>
</li>
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="140" data-name="Cleaning & polish" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">Cleaning & polish</span>
<span class="row-note">Scale, polish and fluoride finish</span>
</span>
<span class="row-price">$140</span>
</label>
</li>
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="65" data-name="Bitewing X-rays" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">Bitewing X-rays</span>
<span class="row-note">Set of 4 low-dose images</span>
</span>
<span class="row-price">$65</span>
</label>
</li>
</ul>
</div>
<div class="card group" data-group="Restorative">
<div class="group-head">
<h2>Restorative</h2>
<p>Repair and rebuild damaged teeth.</p>
</div>
<ul class="rows" role="list">
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="220" data-name="Composite filling" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">Composite filling</span>
<span class="row-note">Tooth-colored, single surface</span>
</span>
<span class="row-price">$220</span>
</label>
</li>
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="1150" data-name="Porcelain crown" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">Porcelain crown</span>
<span class="row-note">Custom-milled, shade matched</span>
</span>
<span class="row-price">$1,150</span>
</label>
</li>
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="890" data-name="Root canal (molar)" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">Root canal (molar)</span>
<span class="row-note">Includes temporary filling</span>
</span>
<span class="row-price">$890</span>
</label>
</li>
</ul>
</div>
<div class="card group" data-group="Cosmetic">
<div class="group-head">
<h2>Cosmetic</h2>
<p>Elective treatments for a brighter smile.</p>
</div>
<ul class="rows" role="list">
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="380" data-name="In-office whitening" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">In-office whitening</span>
<span class="row-note">Single 60-minute session</span>
</span>
<span class="row-price">$380</span>
</label>
</li>
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="1250" data-name="Porcelain veneer" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">Porcelain veneer</span>
<span class="row-note">Per tooth, hand-layered</span>
</span>
<span class="row-price">$1,250</span>
</label>
</li>
<li>
<label class="row">
<input type="checkbox" class="tx" data-price="260" data-name="Cosmetic bonding" />
<span class="check" aria-hidden="true"></span>
<span class="row-body">
<span class="row-name">Cosmetic bonding</span>
<span class="row-note">Chip & gap repair, per tooth</span>
</span>
<span class="row-price">$260</span>
</label>
</li>
</ul>
</div>
</section>
<!-- RIGHT: live summary -->
<aside class="summary" aria-label="Estimate summary">
<div class="card summary-card">
<h2 class="summary-title">Your estimate</h2>
<div class="count-line">
<span class="count-label" id="countLabel">0 treatments selected</span>
</div>
<div class="slider-block">
<div class="slider-top">
<label for="coverage">Insurance coverage</label>
<output id="coverageOut" for="coverage">60%</output>
</div>
<input type="range" id="coverage" min="0" max="100" step="5" value="60"
aria-describedby="coverageHint" />
<p class="slider-hint" id="coverageHint">Paying most out of pocket</p>
</div>
<dl class="totals">
<div class="total-row">
<dt>Plan subtotal</dt>
<dd id="subtotal">$0</dd>
</div>
<div class="total-row muted">
<dt>Insurance covers <span class="badge" id="saveBadge" hidden>–$0</span></dt>
<dd id="covered">–$0</dd>
</div>
<div class="total-row grand">
<dt>Out of pocket</dt>
<dd id="oop">$0</dd>
</div>
</dl>
<div class="bar" aria-hidden="true">
<span class="bar-fill" id="barFill" style="width:0%"></span>
</div>
<p class="bar-cap"><span id="barCap">Select treatments to see your split</span></p>
<div class="actions">
<button type="button" class="btn primary" id="copyBtn">Copy summary</button>
<button type="button" class="btn ghost" id="resetBtn">Reset</button>
</div>
<p class="fineprint">Figures are illustrative estimates and not a quote.</p>
</div>
</aside>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>The Dental Cost Estimator is a clean, clinical two-column tool that helps patients understand what a treatment plan might cost before they ever sit in the chair. On the left, procedures are grouped into cards — Preventive, Restorative, and Cosmetic — each row a large tappable checkbox with a plain-language name, a short note, and a transparent price. Ticking a row lifts it with a soft accent wash and adds its price to the running plan.
On the right, a sticky summary panel does the math in real time. An insurance coverage slider (0–100%) glides between “Paying out of pocket” and “Fully covered,” recoloring a progress track as it moves. Every change animates the subtotal, the estimated insurance contribution, and the final out-of-pocket figure with a quick count-up, while a savings badge highlights how much insurance is trimming from the bill. Buttons let patients reset the plan or copy a shareable summary, and a toast confirms the action.
Everything is keyboard-navigable with visible focus rings, the layout collapses to a single column under 520px with the summary dropping below the list, and all figures use realistic but fictional pricing so the piece feels believable without pretending to be a real quote.
Illustrative UI only — not intended for real medical use.