Vet — Vaccination Tracker
A friendly veterinary vaccination panel for one pet that lists core and non-core vaccines with up-to-date, due-soon and overdue badges, last-given and next-due dates, an animated coverage progress ring, and an inline schedule action that clears overdue items and recalculates coverage live.
MCP
Code
:root {
--teal: #179c8e;
--teal-d: #0f7f73;
--teal-50: #e6f5f2;
--coral: #ff8a5c;
--ink: #173a36;
--muted: #6b827e;
--bg: #f2f8f6;
--white: #ffffff;
--ok: #179c8e;
--soon: #d98b2b;
--over: #e0613f;
--radius: 22px;
--radius-sm: 14px;
--shadow: 0 14px 38px -18px rgba(15, 127, 115, 0.45);
--font: "Inter", system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
display: flex;
justify-content: center;
padding: 32px 16px;
min-height: 100vh;
}
.tracker {
width: 100%;
max-width: 560px;
background: var(--white);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 28px;
}
/* Header */
.tracker__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.pet {
display: flex;
align-items: center;
gap: 14px;
}
.pet__avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: var(--teal-50);
display: grid;
place-items: center;
font-size: 30px;
flex-shrink: 0;
}
.pet__eyebrow {
margin: 0;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--teal-d);
}
.pet__name {
margin: 2px 0;
font-size: 24px;
font-weight: 700;
}
.pet__sub {
margin: 0;
font-size: 13px;
color: var(--muted);
}
/* Coverage ring */
.coverage {
position: relative;
width: 104px;
height: 104px;
flex-shrink: 0;
}
.ring {
transform: rotate(-90deg);
}
.ring__track {
fill: none;
stroke: var(--teal-50);
stroke-width: 11;
}
.ring__value {
fill: none;
stroke: var(--teal);
stroke-width: 11;
stroke-linecap: round;
stroke-dasharray: 326.7;
stroke-dashoffset: 326.7;
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1), stroke 0.4s ease;
}
.coverage__label {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
}
.coverage__label strong {
font-size: 22px;
font-weight: 700;
color: var(--ink);
}
.coverage__label span {
font-size: 11px;
color: var(--muted);
}
/* Summary */
.summary {
list-style: none;
margin: 22px 0 6px;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.summary__item {
background: var(--bg);
border-radius: var(--radius-sm);
padding: 12px;
text-align: center;
border: 1px solid transparent;
}
.summary__item--ok {
border-color: rgba(23, 156, 142, 0.25);
}
.summary__item--soon {
border-color: rgba(217, 139, 43, 0.25);
}
.summary__item--over {
border-color: rgba(224, 97, 63, 0.25);
}
.summary__num {
display: block;
font-size: 22px;
font-weight: 700;
}
.summary__item--ok .summary__num {
color: var(--ok);
}
.summary__item--soon .summary__num {
color: var(--soon);
}
.summary__item--over .summary__num {
color: var(--over);
}
.summary__cap {
font-size: 12px;
color: var(--muted);
}
.live {
margin: 4px 0 0;
min-height: 18px;
font-size: 13px;
font-weight: 600;
color: var(--teal-d);
}
/* Groups */
.group {
margin-top: 22px;
}
.group__title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--muted);
margin: 0 0 10px;
}
.paw {
font-size: 15px;
}
.vax {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
/* Vaccine row */
.row {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px 14px;
align-items: center;
background: var(--white);
border: 1px solid #e4efec;
border-radius: var(--radius-sm);
padding: 14px 16px;
transition: border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.3s ease;
}
.row--over {
border-color: rgba(224, 97, 63, 0.4);
background: #fff6f3;
}
.row__name {
font-size: 15px;
font-weight: 600;
margin: 0;
}
.row__dates {
grid-column: 1 / 2;
margin: 4px 0 0;
font-size: 12.5px;
color: var(--muted);
display: flex;
flex-wrap: wrap;
gap: 4px 14px;
}
.row__dates b {
color: var(--ink);
font-weight: 600;
}
.badge {
justify-self: end;
font-size: 11.5px;
font-weight: 600;
padding: 4px 11px;
border-radius: 999px;
white-space: nowrap;
}
.badge--ok {
background: var(--teal-50);
color: var(--teal-d);
}
.badge--soon {
background: #fdf0dd;
color: var(--soon);
}
.badge--over {
background: #fde3db;
color: var(--over);
}
.row__action {
grid-column: 2 / 3;
grid-row: 2 / 3;
justify-self: end;
}
.btn {
font-family: inherit;
font-size: 13px;
font-weight: 600;
border: none;
border-radius: 999px;
padding: 7px 16px;
cursor: pointer;
color: var(--white);
background: var(--teal);
transition: background 0.2s ease, transform 0.1s ease;
}
.btn:hover {
background: var(--teal-d);
}
.btn:active {
transform: scale(0.96);
}
.btn--confirm {
background: var(--coral);
}
.btn--confirm:hover {
background: #f5743f;
}
.btn:focus-visible,
.row:focus-within {
outline: 3px solid rgba(23, 156, 142, 0.35);
outline-offset: 2px;
}
.row--done {
opacity: 0.92;
}
.tracker__foot {
margin-top: 24px;
padding-top: 16px;
border-top: 1px dashed #d6e7e2;
font-size: 12.5px;
color: var(--muted);
text-align: center;
}
/* Responsive */
@media (max-width: 520px) {
body {
padding: 16px 10px;
}
.tracker {
padding: 20px;
}
.tracker__head {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.coverage {
align-self: center;
}
.summary {
gap: 8px;
}
.summary__num {
font-size: 19px;
}
.row {
grid-template-columns: 1fr;
}
.badge {
justify-self: start;
}
.row__action {
grid-column: 1 / 2;
grid-row: auto;
justify-self: stretch;
}
.btn {
width: 100%;
}
}(function () {
"use strict";
var DAY = 86400000;
var today = new Date(2026, 5, 22); // 2026-06-22, matches resource date
// Seed data: status is derived from nextDue, but we store last/next as offsets in days.
var vaccines = [
{ id: "rabies", name: "Rabies", core: true, last: -380, next: -22 },
{ id: "dhpp", name: "DHPP (Distemper / Parvo)", core: true, last: -120, next: 245 },
{ id: "adeno", name: "Canine Adenovirus", core: true, last: -120, next: 245 },
{ id: "lepto", name: "Leptospirosis", core: false, last: -340, next: 18 },
{ id: "borde", name: "Bordetella (Kennel Cough)", core: false, last: -200, next: -6 },
{ id: "lyme", name: "Lyme Disease", core: false, last: -90, next: 275 },
{ id: "influ", name: "Canine Influenza", core: false, last: -150, next: 41 },
];
var SOON_WINDOW = 30; // days
var liveRegion = document.getElementById("liveRegion");
var coreList = document.getElementById("coreList");
var nonList = document.getElementById("nonList");
var ringValue = document.querySelector(".ring__value");
var coveragePct = document.getElementById("coveragePct");
var CIRC = 2 * Math.PI * 52;
function addDays(offset) {
return new Date(today.getTime() + offset * DAY);
}
function fmt(date) {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}
function statusOf(v) {
if (v.next < 0) return "over";
if (v.next <= SOON_WINDOW) return "soon";
return "ok";
}
var LABELS = { ok: "Up to date", soon: "Due soon", over: "Overdue" };
function announce(msg) {
liveRegion.textContent = msg;
}
function buildRow(v) {
var status = statusOf(v);
var li = document.createElement("li");
li.className = "row" + (status === "over" ? " row--over" : "");
li.setAttribute("tabindex", "0");
li.dataset.id = v.id;
var name = document.createElement("p");
name.className = "row__name";
name.textContent = v.name;
var badge = document.createElement("span");
badge.className = "badge badge--" + status;
badge.textContent = LABELS[status];
var dates = document.createElement("p");
dates.className = "row__dates";
dates.innerHTML =
"Last given: <b>" +
fmt(addDays(v.last)) +
"</b><span>Next due: <b>" +
fmt(addDays(v.next)) +
"</b></span>";
li.appendChild(name);
li.appendChild(badge);
li.appendChild(dates);
if (status !== "ok") {
var slot = document.createElement("div");
slot.className = "row__action";
var btn = document.createElement("button");
btn.className = "btn";
btn.type = "button";
btn.textContent = "Schedule";
attachSchedule(btn, v, li);
slot.appendChild(btn);
li.appendChild(slot);
}
return li;
}
function attachSchedule(btn, v, li) {
var confirming = false;
btn.addEventListener("click", function () {
if (!confirming) {
confirming = true;
btn.classList.add("btn--confirm");
btn.textContent = "Confirm visit?";
announce("Confirm scheduling " + v.name + " for Biscuit?");
setTimeout(function () {
if (confirming) {
confirming = false;
btn.classList.remove("btn--confirm");
btn.textContent = "Schedule";
}
}, 3500);
return;
}
// Confirmed: mark up to date, advance next due by ~1 year.
v.last = 0;
v.next = 365;
li.classList.add("row--done");
announce(v.name + " scheduled — marked up to date, next due " + fmt(addDays(365)) + ".");
setTimeout(render, 120);
});
}
function setRing(pct) {
var offset = CIRC * (1 - pct / 100);
ringValue.style.strokeDashoffset = String(offset);
ringValue.style.stroke = pct >= 75 ? "var(--teal)" : pct >= 50 ? "#d98b2b" : "var(--coral)";
coveragePct.textContent = Math.round(pct) + "%";
}
function render() {
coreList.innerHTML = "";
nonList.innerHTML = "";
var counts = { ok: 0, soon: 0, over: 0 };
vaccines.forEach(function (v) {
counts[statusOf(v)]++;
(v.core ? coreList : nonList).appendChild(buildRow(v));
});
document.querySelector('[data-summary="ok"]').textContent = counts.ok;
document.querySelector('[data-summary="soon"]').textContent = counts.soon;
document.querySelector('[data-summary="over"]').textContent = counts.over;
var pct = (counts.ok / vaccines.length) * 100;
setRing(pct);
}
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vaccination Tracker</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="tracker" aria-labelledby="tracker-title">
<header class="tracker__head">
<div class="pet">
<div class="pet__avatar" aria-hidden="true">🐶</div>
<div class="pet__meta">
<p class="pet__eyebrow">Vaccination record</p>
<h1 id="tracker-title" class="pet__name">Biscuit</h1>
<p class="pet__sub">Beagle · 3 yrs · Patient #VT-2048</p>
</div>
</div>
<div class="coverage" role="img" aria-label="Overall coverage">
<svg class="ring" viewBox="0 0 120 120" width="104" height="104">
<circle class="ring__track" cx="60" cy="60" r="52" />
<circle class="ring__value" cx="60" cy="60" r="52" />
</svg>
<div class="coverage__label">
<strong id="coveragePct">0%</strong>
<span>covered</span>
</div>
</div>
</header>
<ul class="summary" aria-label="Status summary">
<li class="summary__item summary__item--ok">
<span class="summary__num" data-summary="ok">0</span>
<span class="summary__cap">Up to date</span>
</li>
<li class="summary__item summary__item--soon">
<span class="summary__num" data-summary="soon">0</span>
<span class="summary__cap">Due soon</span>
</li>
<li class="summary__item summary__item--over">
<span class="summary__num" data-summary="over">0</span>
<span class="summary__cap">Overdue</span>
</li>
</ul>
<p class="live" id="liveRegion" role="status" aria-live="polite"></p>
<section class="group" aria-labelledby="group-core">
<h2 id="group-core" class="group__title">
<span class="paw" aria-hidden="true">🐾</span> Core vaccines
</h2>
<ul class="vax" id="coreList"></ul>
</section>
<section class="group" aria-labelledby="group-non">
<h2 id="group-non" class="group__title">
<span class="paw" aria-hidden="true">🐾</span> Non-core vaccines
</h2>
<ul class="vax" id="nonList"></ul>
</section>
<footer class="tracker__foot">
Next clinic reminder will be emailed 14 days before each due date.
</footer>
</main>
<script src="script.js"></script>
</body>
</html>Vaccination Tracker
A warm, approachable health panel for a single pet. The header introduces the patient — a beagle named Biscuit — alongside an animated SVG progress ring that reports overall vaccination coverage. Below it, vaccines are grouped into Core and Non-core sections, each row carrying a status badge (Up to date, Due soon or Overdue), the last-given date and the next-due date.
Overdue and due-soon rows surface a teal Schedule button. Clicking it confirms inline, marks the
vaccine up to date, advances its next-due date, and recalculates the coverage ring and the summary
counts — all in vanilla JS with no libraries. Status changes are announced via an aria-live
region for screen-reader users.
The layout uses soft teal and coral accents, large rounded cards and a paw motif, collapsing to a single comfortable column on narrow screens.
Illustrative UI only — not for real veterinary use.