Dental — Tooth Chart
A calm, clinical odontogram for charting a full adult dentition. Click any of the 32 numbered teeth in the SVG arch to select it, then flag its condition — healthy, filling, crown, or missing — each rendered in a distinct color. A live legend tallies every state, a per-tooth notes panel captures clinical remarks, and reassuring toasts confirm each change with soft, trust-first motion.
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.1);
--line-2: rgba(15, 39, 64, 0.18);
--ok: #22b07d;
--warn: #d98a2b;
--danger: #e05252;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
/* condition colors */
--c-healthy: #34d399;
--c-filling: #2563eb;
--c-crown: #d98a2b;
--c-missing: #e05252;
--c-unset: #dbe6f4;
--shadow-sm: 0 1px 2px rgba(15, 39, 64, 0.06), 0 2px 8px rgba(15, 39, 64, 0.05);
--shadow-md: 0 6px 22px rgba(15, 39, 64, 0.08), 0 2px 6px rgba(15, 39, 64, 0.05);
}
* {
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(1200px 600px at 80% -10%, var(--blue-50), transparent 60%),
radial-gradient(900px 500px at -10% 110%, var(--mint-50), transparent 55%),
var(--bg);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 28px 20px 64px;
}
/* Header */
.head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 22px;
}
.head-id {
display: flex;
align-items: center;
gap: 14px;
}
.logo {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 13px;
color: var(--white);
background: linear-gradient(150deg, var(--blue), var(--blue-d));
box-shadow: var(--shadow-sm);
flex: none;
}
.head h1 {
margin: 0;
font-size: 1.28rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.sub {
margin: 2px 0 0;
font-size: 0.86rem;
color: var(--muted);
}
.sub strong {
color: var(--ink-2);
}
.head-actions {
display: flex;
align-items: center;
gap: 10px;
}
.pill {
font-size: 0.76rem;
font-weight: 600;
padding: 6px 12px;
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.pill-ok {
color: var(--ok);
background: var(--mint-50);
}
.pill-ok::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 3px rgba(34, 176, 125, 0.18);
}
.btn {
font: inherit;
font-weight: 600;
font-size: 0.85rem;
border: 0;
border-radius: var(--r-sm);
padding: 9px 15px;
cursor: pointer;
transition: transform 0.12s ease, background 0.16s ease, box-shadow 0.16s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn-ghost {
background: var(--white);
color: var(--ink-2);
border: 1px solid var(--line-2);
box-shadow: var(--shadow-sm);
}
.btn-ghost:hover {
background: var(--blue-50);
color: var(--blue-d);
border-color: var(--blue);
}
.btn:focus-visible,
.cond-btn:focus-visible {
outline: 3px solid rgba(37, 99, 235, 0.4);
outline-offset: 2px;
}
/* Layout grid */
.grid {
display: grid;
grid-template-columns: 1fr 320px;
gap: 20px;
align-items: start;
}
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-md);
padding: 22px;
}
.card h2 {
margin: 0;
font-size: 1.02rem;
font-weight: 700;
letter-spacing: -0.01em;
}
/* Chart */
.chart-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.hint {
margin: 0;
font-size: 0.82rem;
color: var(--muted);
}
.arch {
background: linear-gradient(180deg, #fbfdff, #f4f9ff);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 12px;
}
.arch-label {
display: flex;
justify-content: space-between;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
padding: 0 6px 8px;
}
.arch-label-b {
padding: 8px 6px 0;
}
.arch-svg {
display: block;
width: 100%;
height: auto;
}
.arch-mid {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 6px;
}
.midline {
flex: 1;
height: 1px;
background: repeating-linear-gradient(90deg, var(--line-2) 0 6px, transparent 6px 12px);
}
.midtag {
font-size: 0.68rem;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* Tooth (SVG) */
.tooth {
cursor: pointer;
}
.tooth .tooth-body {
fill: var(--white);
stroke: var(--line-2);
stroke-width: 1.5;
transition: fill 0.2s ease, stroke 0.16s ease, transform 0.16s ease;
transform-box: fill-box;
transform-origin: center;
}
.tooth:hover .tooth-body {
stroke: var(--blue);
transform: translateY(-2px);
}
.tooth .tooth-num {
font-size: 11px;
font-weight: 700;
fill: var(--ink-2);
text-anchor: middle;
dominant-baseline: middle;
pointer-events: none;
}
.tooth[data-cond="healthy"] .tooth-body { fill: var(--c-healthy); stroke: #1f9d6f; }
.tooth[data-cond="filling"] .tooth-body { fill: var(--c-filling); stroke: var(--blue-d); }
.tooth[data-cond="crown"] .tooth-body { fill: var(--c-crown); stroke: #b46f1c; }
.tooth[data-cond="missing"] .tooth-body {
fill: var(--c-missing);
stroke: #b83b3b;
opacity: 0.55;
stroke-dasharray: 4 3;
}
.tooth[data-cond="healthy"] .tooth-num,
.tooth[data-cond="filling"] .tooth-num,
.tooth[data-cond="crown"] .tooth-num,
.tooth[data-cond="missing"] .tooth-num {
fill: var(--white);
}
.tooth.is-selected .tooth-body {
stroke-width: 3;
stroke: var(--blue-d);
filter: drop-shadow(0 3px 6px rgba(37, 99, 235, 0.35));
transform: translateY(-3px);
}
.tooth:focus {
outline: none;
}
.tooth:focus-visible .tooth-body {
stroke: var(--blue-d);
stroke-width: 3;
filter: drop-shadow(0 0 0 3px rgba(37, 99, 235, 0.35));
}
.tooth .tooth-ring {
fill: none;
stroke: var(--blue);
stroke-width: 2;
opacity: 0;
transition: opacity 0.15s ease;
}
.tooth:focus-visible .tooth-ring {
opacity: 1;
}
/* Condition panel */
.selected-line {
margin: 12px 0 12px;
}
.selected-chip {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.86rem;
font-weight: 600;
color: var(--ink-2);
background: var(--blue-50);
border-radius: 999px;
padding: 7px 14px;
}
.selected-chip.is-empty {
color: var(--muted);
background: #f2f5f9;
}
.cond-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.cond-btn {
display: inline-flex;
align-items: center;
gap: 8px;
font: inherit;
font-size: 0.84rem;
font-weight: 600;
color: var(--ink-2);
background: var(--white);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 11px;
cursor: pointer;
transition: transform 0.12s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
}
.cond-btn:not(:disabled):hover {
border-color: var(--blue);
background: var(--blue-50);
transform: translateY(-1px);
}
.cond-btn:active {
transform: translateY(0);
}
.cond-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.cond-btn.is-active {
border-color: var(--blue);
background: var(--blue-50);
color: var(--blue-d);
box-shadow: inset 0 0 0 1px var(--blue);
}
.swatch {
width: 14px;
height: 14px;
border-radius: 5px;
flex: none;
box-shadow: inset 0 0 0 1px rgba(15, 39, 64, 0.12);
}
.swatch-healthy { background: var(--c-healthy); }
.swatch-filling { background: var(--c-filling); }
.swatch-crown { background: var(--c-crown); }
.swatch-missing { background: var(--c-missing); }
.swatch-unset { background: var(--c-unset); }
.notes-label {
display: block;
margin: 16px 0 6px;
font-size: 0.8rem;
font-weight: 600;
color: var(--ink-2);
}
.notes-for {
color: var(--blue-d);
}
.notes {
width: 100%;
font: inherit;
font-size: 0.86rem;
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 12px;
resize: vertical;
background: #fbfdff;
transition: border-color 0.16s ease, box-shadow 0.16s ease;
}
.notes:focus {
outline: none;
border-color: var(--blue);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18);
}
.notes:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Legend */
.rail {
display: grid;
gap: 20px;
}
.legend {
list-style: none;
margin: 14px 0 0;
padding: 0;
display: grid;
gap: 4px;
}
.legend li {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--r-sm);
transition: background 0.15s ease;
}
.legend li:hover {
background: var(--blue-50);
}
.lg-name {
flex: 1;
font-size: 0.86rem;
font-weight: 500;
color: var(--ink-2);
}
.lg-count {
font-size: 0.82rem;
font-weight: 700;
color: var(--ink);
min-width: 26px;
text-align: center;
background: #f2f5f9;
border-radius: 999px;
padding: 2px 8px;
}
.legend li.bump .lg-count {
animation: bump 0.35s ease;
}
@keyframes bump {
0% { transform: scale(1); }
40% { transform: scale(1.28); background: var(--blue-50); color: var(--blue-d); }
100% { transform: scale(1); }
}
.charted-bar {
margin: 16px 0 8px;
height: 8px;
border-radius: 999px;
background: #eef3f9;
overflow: hidden;
}
.charted-bar span {
display: block;
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--blue), var(--mint));
transition: width 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.charted-text {
margin: 0;
font-size: 0.82rem;
color: var(--muted);
}
.charted-text strong {
color: var(--ink);
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--ink);
color: var(--white);
font-size: 0.86rem;
font-weight: 600;
padding: 11px 18px;
border-radius: var(--r-md);
box-shadow: 0 10px 30px rgba(15, 39, 64, 0.28);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 40;
display: inline-flex;
align-items: center;
gap: 9px;
max-width: 90vw;
}
.toast::before {
content: "";
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--mint);
flex: none;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* Responsive */
@media (max-width: 860px) {
.grid {
grid-template-columns: 1fr;
}
.rail {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 520px) {
.wrap {
padding: 20px 14px 56px;
}
.head {
gap: 12px;
}
.head h1 {
font-size: 1.08rem;
}
.head-actions {
width: 100%;
justify-content: space-between;
}
.card {
padding: 18px;
}
.rail {
grid-template-columns: 1fr;
}
.chart-head {
flex-direction: column;
gap: 2px;
}
}(function () {
"use strict";
var SVGNS = "http://www.w3.org/2000/svg";
// Universal numbering system 1..32.
// 1-16 = upper (right molar -> left molar), 17-32 = lower (left molar -> right molar).
var TOOTH_NAMES = {
"3rd molar": [1, 16, 17, 32],
"2nd molar": [2, 15, 18, 31],
"1st molar": [3, 14, 19, 30],
"2nd premolar": [4, 13, 20, 29],
"1st premolar": [5, 12, 21, 28],
canine: [6, 11, 22, 27],
"lateral incisor": [7, 10, 23, 26],
"central incisor": [8, 9, 24, 25]
};
var COND_LABELS = {
healthy: "Healthy",
filling: "Filling",
crown: "Crown",
missing: "Missing"
};
// Pre-charted findings so the chart opens with realistic data.
var SEED = {
2: { cond: "filling", note: "MO amalgam, sound margins." },
3: { cond: "crown", note: "PFM crown placed 2023." },
14: { cond: "filling", note: "Occlusal composite." },
19: { cond: "missing", note: "Extracted; implant candidate." },
30: { cond: "crown" },
9: { cond: "healthy" }
};
var state = {}; // toothNum -> { cond, note }
var selected = null;
var upper = document.getElementById("upperArch");
var lower = document.getElementById("lowerArch");
var selChip = document.getElementById("selChip");
var condGrid = document.getElementById("condGrid");
var condBtns = Array.prototype.slice.call(condGrid.querySelectorAll(".cond-btn"));
var notes = document.getElementById("notes");
var notesFor = document.getElementById("notesFor");
var legend = document.getElementById("legend");
var chartedFill = document.getElementById("chartedFill");
var chartedNum = document.getElementById("chartedNum");
var toastEl = document.getElementById("toast");
var resetBtn = document.getElementById("resetBtn");
var toothEls = {}; // num -> <g>
function nameFor(num) {
for (var key in TOOTH_NAMES) {
if (TOOTH_NAMES[key].indexOf(num) !== -1) return key;
}
return "tooth";
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toast._t);
toast._t = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
// Build one tooth <g> with body, number and focus ring.
function makeTooth(num, x) {
var w = 44;
var y = 24;
var h = 96;
var g = document.createElementNS(SVGNS, "g");
g.setAttribute("class", "tooth");
g.setAttribute("role", "listitem");
g.setAttribute("tabindex", "0");
g.dataset.num = String(num);
updateAria(g, num);
var body = document.createElementNS(SVGNS, "rect");
body.setAttribute("class", "tooth-body");
body.setAttribute("x", x);
body.setAttribute("y", y);
body.setAttribute("width", w);
body.setAttribute("height", h);
body.setAttribute("rx", "13");
body.setAttribute("ry", "15");
var ring = document.createElementNS(SVGNS, "rect");
ring.setAttribute("class", "tooth-ring");
ring.setAttribute("x", x - 3);
ring.setAttribute("y", y - 3);
ring.setAttribute("width", w + 6);
ring.setAttribute("height", h + 6);
ring.setAttribute("rx", "16");
var label = document.createElementNS(SVGNS, "text");
label.setAttribute("class", "tooth-num");
label.setAttribute("x", x + w / 2);
label.setAttribute("y", y + h / 2);
label.textContent = String(num);
g.appendChild(ring);
g.appendChild(body);
g.appendChild(label);
g.addEventListener("click", function () {
selectTooth(num);
});
g.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
selectTooth(num);
}
});
return g;
}
function updateAria(g, num) {
var s = state[num];
var cond = s && s.cond ? COND_LABELS[s.cond] : "not charted";
g.setAttribute("aria-label", "Tooth " + num + ", " + nameFor(num) + ", " + cond);
}
function buildArch(svg, nums) {
var startX = 14;
var step = 54;
nums.forEach(function (num, i) {
var g = makeTooth(num, startX + i * step);
svg.appendChild(g);
toothEls[num] = g;
});
}
function selectTooth(num) {
selected = num;
for (var n in toothEls) {
toothEls[n].classList.toggle("is-selected", Number(n) === num);
}
var el = toothEls[num];
if (el) el.focus({ preventScroll: true });
selChip.classList.remove("is-empty");
selChip.textContent = "Tooth " + num + " · " + nameFor(num);
var s = state[num] || {};
condBtns.forEach(function (b) {
b.disabled = false;
b.classList.toggle("is-active", b.dataset.cond === s.cond);
});
notes.disabled = false;
notes.value = s.note || "";
notes.placeholder = "Add a clinical note for tooth " + num + "…";
notesFor.textContent = "· tooth " + num;
}
function setCondition(cond) {
if (selected == null) return;
var s = state[selected] || (state[selected] = {});
s.cond = cond;
var el = toothEls[selected];
el.dataset.cond = cond;
updateAria(el, selected);
condBtns.forEach(function (b) {
b.classList.toggle("is-active", b.dataset.cond === cond);
});
refreshSummary(cond);
toast("Tooth " + selected + " marked " + COND_LABELS[cond].toLowerCase());
}
function refreshSummary(bumpCond) {
var counts = { healthy: 0, filling: 0, crown: 0, missing: 0, unset: 0 };
var charted = 0;
for (var i = 1; i <= 32; i++) {
var s = state[i];
if (s && s.cond) {
counts[s.cond]++;
charted++;
} else {
counts.unset++;
}
}
Object.keys(counts).forEach(function (k) {
var cell = legend.querySelector('[data-count="' + k + '"]');
if (cell) cell.textContent = counts[k];
});
if (bumpCond) {
var li = legend.querySelector('li[data-cond="' + bumpCond + '"]');
if (li) {
li.classList.remove("bump");
void li.offsetWidth; // reflow to restart animation
li.classList.add("bump");
}
}
chartedNum.textContent = String(charted);
chartedFill.style.width = (charted / 32) * 100 + "%";
}
// Wire condition buttons
condBtns.forEach(function (b) {
b.addEventListener("click", function () {
setCondition(b.dataset.cond);
});
});
// Notes persistence per tooth
notes.addEventListener("input", function () {
if (selected == null) return;
var s = state[selected] || (state[selected] = {});
s.note = notes.value;
});
notes.addEventListener("blur", function () {
if (selected != null && notes.value.trim()) {
toast("Note saved for tooth " + selected);
}
});
resetBtn.addEventListener("click", function () {
state = {};
for (var n in toothEls) {
delete toothEls[n].dataset.cond;
toothEls[n].classList.remove("is-selected");
updateAria(toothEls[n], Number(n));
}
selected = null;
selChip.classList.add("is-empty");
selChip.textContent = "No tooth selected";
condBtns.forEach(function (b) {
b.disabled = true;
b.classList.remove("is-active");
});
notes.value = "";
notes.disabled = true;
notes.placeholder = "Select a tooth to add a note…";
notesFor.textContent = "";
refreshSummary();
toast("Chart reset — 32 teeth cleared");
});
// Init
buildArch(upper, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
buildArch(lower, [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]);
// Apply seed findings
Object.keys(SEED).forEach(function (k) {
var num = Number(k);
state[num] = { cond: SEED[k].cond, note: SEED[k].note || "" };
toothEls[num].dataset.cond = SEED[k].cond;
updateAria(toothEls[num], num);
});
refreshSummary();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dental — Tooth Chart</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="head">
<div class="head-id">
<span class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3c-3 0-5 1.5-5 4 0 1.6.7 2.6 1 4 .4 1.8.3 3.4.7 5 .3 1.3.7 2.6 1.6 2.6.8 0 1-.9 1.2-2.2.2-1.1.3-2 1.5-2s1.3.9 1.5 2c.2 1.3.4 2.2 1.2 2.2.9 0 1.3-1.3 1.6-2.6.4-1.6.3-3.2.7-5 .3-1.4 1-2.4 1-4 0-2.5-2-4-5-4a6 6 0 0 0-3 .8" />
</svg>
</span>
<div>
<h1>Northbridge Dental — Odontogram</h1>
<p class="sub">Patient: <strong>Marlowe Quinn</strong> · Chart #DC-4821 · Adult dentition (1–32)</p>
</div>
</div>
<div class="head-actions">
<span class="pill pill-ok" id="patientStatus">Exam in progress</span>
<button class="btn btn-ghost" id="resetBtn" type="button">Reset chart</button>
</div>
</header>
<div class="grid">
<!-- Chart -->
<section class="card chart-card" aria-labelledby="chartTitle">
<div class="chart-head">
<h2 id="chartTitle">Dental arch</h2>
<p class="hint">Select a tooth, then choose a condition.</p>
</div>
<div class="arch" role="group" aria-label="Adult dental arch, 32 teeth">
<div class="arch-label"><span>Upper — Maxillary</span><span>R · L</span></div>
<svg class="arch-svg" id="upperArch" viewBox="0 0 880 150" role="list" aria-label="Upper arch, teeth 1 to 16"></svg>
<div class="arch-mid"><span class="midline" aria-hidden="true"></span><span class="midtag">midline</span><span class="midline" aria-hidden="true"></span></div>
<svg class="arch-svg" id="lowerArch" viewBox="0 0 880 150" role="list" aria-label="Lower arch, teeth 17 to 32"></svg>
<div class="arch-label arch-label-b"><span>Lower — Mandibular</span><span>L · R</span></div>
</div>
</section>
<!-- Side rail -->
<aside class="rail">
<section class="card cond-card" aria-labelledby="condTitle">
<h2 id="condTitle">Condition</h2>
<div class="selected-line">
<span class="selected-chip" id="selChip" aria-live="polite">No tooth selected</span>
</div>
<div class="cond-grid" id="condGrid" role="group" aria-label="Set condition for selected tooth">
<button class="cond-btn" data-cond="healthy" type="button" disabled>
<span class="swatch swatch-healthy" aria-hidden="true"></span>Healthy
</button>
<button class="cond-btn" data-cond="filling" type="button" disabled>
<span class="swatch swatch-filling" aria-hidden="true"></span>Filling
</button>
<button class="cond-btn" data-cond="crown" type="button" disabled>
<span class="swatch swatch-crown" aria-hidden="true"></span>Crown
</button>
<button class="cond-btn" data-cond="missing" type="button" disabled>
<span class="swatch swatch-missing" aria-hidden="true"></span>Missing
</button>
</div>
<label class="notes-label" for="notes">Clinical note <span id="notesFor" class="notes-for"></span></label>
<textarea id="notes" class="notes" rows="3" placeholder="Select a tooth to add a note…" disabled></textarea>
</section>
<section class="card legend-card" aria-labelledby="legendTitle">
<h2 id="legendTitle">Legend & summary</h2>
<ul class="legend" id="legend">
<li data-cond="healthy"><span class="swatch swatch-healthy" aria-hidden="true"></span><span class="lg-name">Healthy</span><span class="lg-count" data-count="healthy">0</span></li>
<li data-cond="filling"><span class="swatch swatch-filling" aria-hidden="true"></span><span class="lg-name">Filling</span><span class="lg-count" data-count="filling">0</span></li>
<li data-cond="crown"><span class="swatch swatch-crown" aria-hidden="true"></span><span class="lg-name">Crown</span><span class="lg-count" data-count="crown">0</span></li>
<li data-cond="missing"><span class="swatch swatch-missing" aria-hidden="true"></span><span class="lg-name">Missing</span><span class="lg-count" data-count="missing">0</span></li>
<li data-cond="unset"><span class="swatch swatch-unset" aria-hidden="true"></span><span class="lg-name">Not charted</span><span class="lg-count" data-count="unset">32</span></li>
</ul>
<div class="charted-bar" aria-hidden="true"><span id="chartedFill"></span></div>
<p class="charted-text"><strong id="chartedNum">0</strong> of 32 teeth charted</p>
</section>
</aside>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>The tooth chart presents a full adult dental arch as a single SVG odontogram, with the upper and lower jaws laid out in two rows of sixteen teeth. Each tooth is numbered in the universal 1–32 scheme and rendered as a rounded, tactile shape that lifts on hover and shows a clear focus ring for keyboard users. Selecting a tooth highlights it and opens a condition panel where you can mark it healthy, filling, crown, or missing — every state maps to a calm, accessible color that fills the tooth instantly.
A legend along the side keeps a running count of each condition across the whole mouth, so the overall picture updates the moment you make a change. A notes panel tied to the currently selected tooth lets you jot a short clinical remark, which is remembered per tooth as you move around the arch. Buttons, toasts, and transitions are deliberately soft and reassuring, matching a light clinical aesthetic with lots of white space, rounded corners, and gentle micro-interactions.
Everything is keyboard reachable: teeth are focusable buttons you can trigger with Enter or Space, arrow-free tabbing walks the arch in order, and each action emits an aria-live toast so assistive tech stays in sync. The layout collapses gracefully to a single readable column on narrow screens.
Illustrative UI only — not intended for real medical use.