Vet — Pet Owner Portal
A warm pet-owner dashboard with greeting, pet summary cards showing vaccination status, upcoming appointments, recent visits, outstanding invoices and prescription refills due, plus quick actions to book, refill and message the clinic with live-updating counts and inline confirmations.
MCP
Code
:root {
--teal: #179c8e;
--teal-d: #0f7f73;
--teal-50: #e6f5f2;
--coral: #ff8a5c;
--ink: #173a36;
--muted: #6b827e;
--bg: #f2f8f6;
--white: #ffffff;
--line: #d7eae5;
--ok: #179c8e;
--warn: #e08a2b;
--over: #d8513f;
--radius: 20px;
--radius-sm: 13px;
--shadow: 0 10px 30px rgba(15, 127, 115, 0.08);
--shadow-sm: 0 4px 14px rgba(15, 127, 115, 0.06);
--font: 'Inter', system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
body, h1, h2, h3, p, dl, dd, ul { margin: 0; }
ul { list-style: none; padding: 0; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
.portal { max-width: 1080px; margin: 0 auto; padding: 0 18px 56px; }
/* Topbar */
.topbar {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 4px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand__mark {
width: 40px; height: 40px; display: grid; place-items: center;
background: var(--teal-50); border-radius: 12px; font-size: 20px;
}
.brand__name { font-weight: 800; letter-spacing: -.01em; font-size: 1.05rem; }
.topbar__right { display: flex; align-items: center; gap: 12px; }
.icon-btn {
position: relative; border: 1px solid var(--line); background: var(--white);
width: 42px; height: 42px; border-radius: 12px; font-size: 18px; cursor: pointer;
transition: transform .12s ease, box-shadow .12s ease;
}
.icon-btn:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); }
.dot {
position: absolute; top: 9px; right: 10px; width: 8px; height: 8px;
background: var(--coral); border-radius: 50%; border: 2px solid var(--white);
}
.avatar {
width: 42px; height: 42px; border-radius: 50%; display: grid; place-items: center;
background: linear-gradient(135deg, var(--teal), var(--teal-d));
color: #fff; font-weight: 700; font-size: .85rem;
}
/* Hero */
.hero {
background: linear-gradient(135deg, var(--teal) 0%, var(--teal-d) 100%);
color: #fff; border-radius: var(--radius); padding: 28px;
box-shadow: var(--shadow);
display: flex; align-items: center; justify-content: space-between; gap: 24px;
flex-wrap: wrap;
}
.hero__eyebrow { font-size: .8rem; opacity: .85; font-weight: 500; }
.hero__title { font-size: 1.7rem; font-weight: 800; letter-spacing: -.02em; margin: 4px 0 6px; }
.hero__sub { opacity: .92; font-size: .95rem; }
.hero__sub strong { font-weight: 700; }
.quick-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.qa {
border: 1px solid rgba(255,255,255,.35); background: rgba(255,255,255,.14);
color: #fff; font-family: inherit; font-weight: 600; font-size: .9rem;
padding: 11px 16px; border-radius: 12px; cursor: pointer;
transition: background .14s ease, transform .12s ease;
}
.qa:hover { background: rgba(255,255,255,.26); transform: translateY(-1px); }
.qa--primary { background: var(--coral); border-color: var(--coral); }
.qa--primary:hover { background: #ff7a45; }
/* Blocks */
.block {
background: var(--white); border-radius: var(--radius); padding: 22px;
box-shadow: var(--shadow-sm); margin-top: 18px;
}
.block__head {
display: flex; align-items: center; gap: 12px; margin-bottom: 16px;
}
.block__title { font-size: 1.1rem; font-weight: 700; letter-spacing: -.01em; }
.pill {
margin-left: auto; background: var(--teal-50); color: var(--teal-d);
font-weight: 700; font-size: .82rem; padding: 3px 11px; border-radius: 999px;
}
.balance { margin-left: auto; font-size: .85rem; color: var(--muted); }
.balance strong { color: var(--ink); font-size: 1rem; }
.grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-top: 0;
}
.grid .block { margin-top: 18px; }
/* Pets */
.pets { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.pet {
border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 16px;
background: var(--bg); transition: transform .14s ease, box-shadow .14s ease;
}
.pet:hover { transform: translateY(-2px); box-shadow: var(--shadow-sm); }
.pet__top { display: flex; align-items: flex-start; gap: 12px; }
.pet__photo {
width: 46px; height: 46px; min-width: 46px; display: grid; place-items: center;
background: var(--white); border-radius: 12px; font-size: 24px;
}
.pet__name { font-size: 1.02rem; font-weight: 700; }
.pet__meta { font-size: .8rem; color: var(--muted); }
.vax {
margin-left: auto; font-size: .72rem; font-weight: 700; padding: 4px 9px;
border-radius: 999px; white-space: nowrap; align-self: flex-start;
}
.vax--ok { background: var(--teal-50); color: var(--teal-d); }
.vax--soon { background: #fdf0dd; color: var(--warn); }
.vax--over { background: #fbe3df; color: var(--over); }
.pet__stats {
display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px;
}
.pet__stats dt { font-size: .7rem; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.pet__stats dd { font-size: .85rem; font-weight: 600; margin-top: 2px; }
/* Appointments */
.appts { display: flex; flex-direction: column; gap: 12px; }
.appt {
display: flex; align-items: center; gap: 14px;
border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 12px;
transition: opacity .2s ease;
}
.date {
width: 56px; min-width: 56px; text-align: center; border-radius: 12px;
background: var(--teal-50); color: var(--teal-d); padding: 8px 0;
}
.date__mon { display: block; font-size: .68rem; font-weight: 700; letter-spacing: .06em; }
.date__day { display: block; font-size: 1.4rem; font-weight: 800; line-height: 1; }
.appt__body { flex: 1; min-width: 0; }
.appt__title { font-size: .95rem; font-weight: 700; }
.appt__meta { font-size: .8rem; color: var(--muted); }
.mod {
display: inline-block; margin-top: 5px; font-size: .72rem; font-weight: 700;
padding: 3px 9px; border-radius: 999px;
}
.mod--inperson { background: var(--teal-50); color: var(--teal-d); }
.mod--video { background: #eee9fb; color: #6a4fc7; }
.appt.is-cancelled { opacity: .5; }
.appt.is-cancelled .mod { background: #f0f0f0; color: var(--muted); }
.link-btn {
background: none; border: none; color: var(--teal-d); font-family: inherit;
font-weight: 600; font-size: .85rem; cursor: pointer; padding: 6px 8px;
border-radius: 8px; text-decoration: none;
}
.link-btn:hover { background: var(--teal-50); }
.link-btn.is-confirm { color: var(--over); }
/* Recent */
.recent { display: flex; flex-direction: column; gap: 4px; }
.rv {
display: flex; align-items: center; gap: 12px; padding: 10px 4px;
border-bottom: 1px solid var(--line);
}
.rv:last-child { border-bottom: none; }
.rv__dot { width: 9px; height: 9px; min-width: 9px; border-radius: 50%; background: var(--teal); }
.rv div { flex: 1; min-width: 0; }
.rv__title { font-size: .92rem; font-weight: 700; }
.rv__meta { font-size: .78rem; color: var(--muted); }
/* Invoices */
.invoices { display: flex; flex-direction: column; gap: 10px; }
.inv {
display: flex; align-items: center; gap: 12px;
border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 12px;
}
.inv div { flex: 1; min-width: 0; }
.inv__title { font-size: .9rem; font-weight: 700; }
.inv__meta { font-size: .78rem; color: var(--muted); }
.inv__amt { font-weight: 800; font-size: .95rem; }
.btn-pay {
background: var(--teal); color: #fff; border: none; font-family: inherit;
font-weight: 700; font-size: .82rem; padding: 8px 16px; border-radius: 10px;
cursor: pointer; transition: background .14s ease, transform .12s ease;
}
.btn-pay:hover { background: var(--teal-d); transform: translateY(-1px); }
.inv.is-paid { opacity: .55; }
.inv.is-paid .btn-pay { background: var(--teal-50); color: var(--teal-d); cursor: default; pointer-events: none; }
/* Refills */
.refills { display: flex; flex-direction: column; gap: 10px; }
.rx {
display: flex; align-items: center; gap: 12px;
border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 12px;
}
.rx__icon {
width: 40px; height: 40px; min-width: 40px; display: grid; place-items: center;
background: var(--teal-50); border-radius: 11px; font-size: 18px;
}
.rx__body { flex: 1; min-width: 0; }
.rx__title { font-size: .92rem; font-weight: 700; }
.rx__meta { font-size: .78rem; color: var(--muted); }
.btn-refill {
background: var(--coral); color: #fff; border: none; font-family: inherit;
font-weight: 700; font-size: .82rem; padding: 8px 16px; border-radius: 10px;
cursor: pointer; transition: background .14s ease, transform .12s ease;
}
.btn-refill:hover { background: #ff7a45; transform: translateY(-1px); }
.rx.is-requested .btn-refill { background: var(--teal-50); color: var(--teal-d); cursor: default; pointer-events: none; }
/* Focus */
:focus-visible {
outline: 3px solid rgba(23, 156, 142, .45);
outline-offset: 2px;
}
/* Toast */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 20px);
background: var(--ink); color: #fff; padding: 13px 20px; border-radius: 12px;
font-size: .88rem; font-weight: 600; box-shadow: var(--shadow);
opacity: 0; pointer-events: none; transition: opacity .2s ease, transform .2s ease;
z-index: 50; max-width: calc(100% - 36px);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 880px) {
.grid { grid-template-columns: 1fr; }
.pets { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.portal { padding: 0 12px 40px; }
.hero { padding: 22px; }
.hero__title { font-size: 1.4rem; }
.quick-actions { width: 100%; }
.qa { flex: 1; justify-content: center; }
.block { padding: 18px; }
.vax { margin-left: 0; margin-top: 8px; width: 100%; text-align: center; }
.pet__top { flex-wrap: wrap; }
.appt { flex-wrap: wrap; }
.appt .link-btn { margin-left: auto; }
}(function () {
'use strict';
var toastEl = document.getElementById('toast');
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove('show');
}, 2600);
}
// Live counters --------------------------------------------------------
var apptsList = document.getElementById('apptsList');
var apptsPill = document.getElementById('apptsPill');
var apptCount = document.getElementById('apptCount');
var rxList = document.getElementById('rxList');
var rxPill = document.getElementById('rxPill');
var rxCount = document.getElementById('rxCount') || document.getElementById('refillCount');
function activeAppts() {
return apptsList.querySelectorAll('.appt:not(.is-cancelled)').length;
}
function pendingRefills() {
return rxList.querySelectorAll('.rx:not(.is-requested)').length;
}
function syncCounts() {
var a = activeAppts();
var r = pendingRefills();
if (apptsPill) apptsPill.textContent = a;
if (apptCount) apptCount.textContent = a;
if (rxPill) rxPill.textContent = r;
if (rxCount) rxCount.textContent = r;
}
// Cancel appointment (inline confirm) ----------------------------------
apptsList.addEventListener('click', function (e) {
var btn = e.target.closest('[data-cancel]');
if (!btn) return;
var card = btn.closest('.appt');
if (!card) return;
if (!btn.classList.contains('is-confirm')) {
btn.classList.add('is-confirm');
btn.textContent = 'Confirm cancel?';
setTimeout(function () {
if (btn.classList.contains('is-confirm') && card.parentNode) {
btn.classList.remove('is-confirm');
btn.textContent = 'Cancel';
}
}, 3500);
return;
}
card.classList.add('is-cancelled');
btn.remove();
var mod = card.querySelector('.mod');
if (mod) { mod.textContent = 'Cancelled'; mod.className = 'mod'; }
syncCounts();
toast('Appointment cancelled');
});
// Pay invoice ----------------------------------------------------------
var invList = document.getElementById('invList');
var balanceEl = document.getElementById('balance');
function recalcBalance() {
var total = 0;
invList.querySelectorAll('.inv:not(.is-paid)').forEach(function (li) {
total += parseFloat(li.getAttribute('data-amount')) || 0;
});
if (balanceEl) balanceEl.textContent = '$' + total.toFixed(2);
}
invList.addEventListener('click', function (e) {
var btn = e.target.closest('[data-pay]');
if (!btn) return;
var li = btn.closest('.inv');
if (!li || li.classList.contains('is-paid')) return;
li.classList.add('is-paid');
btn.textContent = 'Paid ✓';
recalcBalance();
toast('Payment received — thank you!');
});
// Request refill -------------------------------------------------------
rxList.addEventListener('click', function (e) {
var btn = e.target.closest('[data-req]');
if (!btn) return;
var li = btn.closest('.rx');
if (!li || li.classList.contains('is-requested')) return;
li.classList.add('is-requested');
btn.textContent = 'Requested ✓';
var meta = li.querySelector('.rx__meta');
if (meta) meta.textContent = 'Refill requested · ready in ~2 hrs';
syncCounts();
toast('Refill request sent to the pharmacy');
});
// Quick actions --------------------------------------------------------
document.querySelectorAll('.qa').forEach(function (qa) {
qa.addEventListener('click', function () {
switch (qa.getAttribute('data-action')) {
case 'book':
toast('Booking flow would open here');
break;
case 'refill': {
var first = rxList.querySelector('.rx:not(.is-requested) [data-req]');
if (first) { first.click(); }
else { toast('No refills pending'); }
break;
}
case 'message':
toast('Opening secure message to the clinic…');
break;
}
});
});
// Recent visit summaries ----------------------------------------------
document.querySelectorAll('[data-summary]').forEach(function (a) {
a.addEventListener('click', function (e) {
e.preventDefault();
var title = a.closest('.rv').querySelector('.rv__title').textContent;
toast('Opening visit summary: ' + title);
});
});
syncCounts();
recalcBalance();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pet Owner Portal — Maple Grove Veterinary</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>
<div class="portal">
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true">🐾</span>
<span class="brand__name">Maple Grove Veterinary</span>
</div>
<div class="topbar__right">
<button class="icon-btn" type="button" aria-label="Notifications">🔔<span class="dot" aria-hidden="true"></span></button>
<div class="avatar" aria-hidden="true">JR</div>
</div>
</header>
<main class="content">
<section class="hero" aria-labelledby="greeting">
<div>
<p class="hero__eyebrow">Sunday, June 22</p>
<h1 id="greeting" class="hero__title">Good morning, Jordan 👋</h1>
<p class="hero__sub">You have <strong id="apptCount">2</strong> upcoming visits and <strong id="refillCount">2</strong> refills to review.</p>
</div>
<div class="quick-actions" role="group" aria-label="Quick actions">
<button class="qa qa--primary" type="button" data-action="book">📅 Book a visit</button>
<button class="qa" type="button" data-action="refill">💊 Request refill</button>
<button class="qa" type="button" data-action="message">💬 Message clinic</button>
</div>
</section>
<section class="block" aria-labelledby="pets-h">
<div class="block__head">
<h2 id="pets-h" class="block__title">My pets</h2>
</div>
<div class="pets">
<article class="pet" data-vax="ok">
<div class="pet__top">
<span class="pet__photo" aria-hidden="true">🐕</span>
<div>
<h3 class="pet__name">Biscuit</h3>
<p class="pet__meta">Golden Retriever · 4 yrs · 31.2 kg</p>
</div>
<span class="vax vax--ok">Vaccines up to date</span>
</div>
<dl class="pet__stats">
<div><dt>Microchip</dt><dd>985 141 003 22</dd></div>
<div><dt>Next due</dt><dd>Rabies · Mar 2027</dd></div>
</dl>
</article>
<article class="pet" data-vax="soon">
<div class="pet__top">
<span class="pet__photo" aria-hidden="true">🐈</span>
<div>
<h3 class="pet__name">Pixel</h3>
<p class="pet__meta">Domestic Shorthair · 2 yrs · 4.6 kg</p>
</div>
<span class="vax vax--soon">FVRCP due in 12 days</span>
</div>
<dl class="pet__stats">
<div><dt>Microchip</dt><dd>985 141 088 47</dd></div>
<div><dt>Next due</dt><dd>FVRCP · Jul 4</dd></div>
</dl>
</article>
<article class="pet" data-vax="over">
<div class="pet__top">
<span class="pet__photo" aria-hidden="true">🐰</span>
<div>
<h3 class="pet__name">Clover</h3>
<p class="pet__meta">Holland Lop Rabbit · 3 yrs · 1.7 kg</p>
</div>
<span class="vax vax--over">Myxomatosis overdue</span>
</div>
<dl class="pet__stats">
<div><dt>Microchip</dt><dd>985 141 120 09</dd></div>
<div><dt>Was due</dt><dd>May 30</dd></div>
</dl>
</article>
</div>
</section>
<div class="grid">
<section class="block" aria-labelledby="appts-h">
<div class="block__head">
<h2 id="appts-h" class="block__title">Upcoming appointments</h2>
<span class="pill" id="apptsPill">2</span>
</div>
<ul class="appts" id="apptsList">
<li class="appt" data-id="a1">
<div class="date">
<span class="date__mon">JUN</span>
<span class="date__day">26</span>
</div>
<div class="appt__body">
<h3 class="appt__title">Pixel — Annual wellness exam</h3>
<p class="appt__meta">Dr. Amara Osei · 10:30 AM</p>
<span class="mod mod--inperson">In person · Room 2</span>
</div>
<button class="link-btn" type="button" data-cancel>Cancel</button>
</li>
<li class="appt" data-id="a2">
<div class="date">
<span class="date__mon">JUL</span>
<span class="date__day">03</span>
</div>
<div class="appt__body">
<h3 class="appt__title">Biscuit — Dental check follow-up</h3>
<p class="appt__meta">Dr. Liam Whitfield · 4:15 PM</p>
<span class="mod mod--video">Video call</span>
</div>
<button class="link-btn" type="button" data-cancel>Cancel</button>
</li>
</ul>
</section>
<section class="block" aria-labelledby="recent-h">
<div class="block__head">
<h2 id="recent-h" class="block__title">Recent visits</h2>
</div>
<ul class="recent">
<li class="rv">
<span class="rv__dot" aria-hidden="true"></span>
<div>
<h3 class="rv__title">Clover — Skin irritation check</h3>
<p class="rv__meta">May 28 · Dr. Amara Osei · Topical cream prescribed</p>
</div>
<a class="link-btn" href="#" data-summary>Summary</a>
</li>
<li class="rv">
<span class="rv__dot" aria-hidden="true"></span>
<div>
<h3 class="rv__title">Biscuit — Dental cleaning</h3>
<p class="rv__meta">May 14 · Dr. Liam Whitfield · No complications</p>
</div>
<a class="link-btn" href="#" data-summary>Summary</a>
</li>
<li class="rv">
<span class="rv__dot" aria-hidden="true"></span>
<div>
<h3 class="rv__title">Pixel — Booster vaccine</h3>
<p class="rv__meta">Apr 02 · Dr. Amara Osei · Rabies + FVRCP</p>
</div>
<a class="link-btn" href="#" data-summary>Summary</a>
</li>
</ul>
</section>
<section class="block" aria-labelledby="inv-h">
<div class="block__head">
<h2 id="inv-h" class="block__title">Outstanding invoices</h2>
<span class="balance">Balance <strong id="balance">$214.50</strong></span>
</div>
<ul class="invoices" id="invList">
<li class="inv" data-amount="86.00">
<div>
<h3 class="inv__title">#INV-2048 · Dental cleaning</h3>
<p class="inv__meta">Issued May 14 · Due Jun 28</p>
</div>
<span class="inv__amt">$86.00</span>
<button class="btn-pay" type="button" data-pay>Pay</button>
</li>
<li class="inv" data-amount="128.50">
<div>
<h3 class="inv__title">#INV-2071 · Skin consult + cream</h3>
<p class="inv__meta">Issued May 28 · Due Jul 12</p>
</div>
<span class="inv__amt">$128.50</span>
<button class="btn-pay" type="button" data-pay>Pay</button>
</li>
</ul>
</section>
<section class="block" aria-labelledby="rx-h">
<div class="block__head">
<h2 id="rx-h" class="block__title">Refills due</h2>
<span class="pill" id="rxPill">2</span>
</div>
<ul class="refills" id="rxList">
<li class="rx" data-id="r1">
<span class="rx__icon" aria-hidden="true">💊</span>
<div class="rx__body">
<h3 class="rx__title">Apoquel 5.4mg — Clover</h3>
<p class="rx__meta">3 days of doses left · Refills: 2</p>
</div>
<button class="btn-refill" type="button" data-req>Request</button>
</li>
<li class="rx" data-id="r2">
<span class="rx__icon" aria-hidden="true">💊</span>
<div class="rx__body">
<h3 class="rx__title">Heartgard Plus — Biscuit</h3>
<p class="rx__meta">Last dose Jun 1 · Refills: 5</p>
</div>
<button class="btn-refill" type="button" data-req>Request</button>
</li>
</ul>
</section>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
</div>
<script src="script.js"></script>
</body>
</html>Pet Owner Portal
A friendly, single-screen client portal for a veterinary practice. It opens with a personal greeting and a row of pet summary cards — each showing species, age, weight and a colour-coded vaccination status badge (up to date, due soon, overdue). Below sit the day-to-day essentials: upcoming appointments with date blocks and modality, recent visit history, outstanding invoices with a running balance, and prescription refills that are due.
Quick-action buttons let owners book a visit, request a refill or message the clinic. Booking and refill requests update the relevant lists and live counts in place, paying an invoice clears its balance with an inline confirmation, and refill cards flip to a Requested state. Everything is warm, rounded and approachable, with accessible headings, labelled controls and visible focus.
All data is realistic placeholder content and every interaction is handled with vanilla JavaScript, no libraries. The layout collapses to a single column under 520px.
Illustrative UI only — not for real veterinary use.