Clinic — My Appointments
A patient appointments panel with a segmented Upcoming/Past tab control, status-badged appointment cards showing date, doctor, time and in-person/video modality, plus inline-confirm cancel, reschedule and view-summary actions that update live counts.
MCP
Codice
:root {
--teal: #129c93;
--teal-d: #0c7a73;
--teal-700: #0a655f;
--teal-50: #e7f5f3;
--coral: #ff7a66;
--coral-soft: #ffe6df;
--ink: #16322f;
--ink-2: #3a534f;
--muted: #6b827e;
--bg: #f1f7f6;
--white: #ffffff;
--line: rgba(16, 50, 47, 0.1);
--line-2: rgba(16, 50, 47, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--pending: #6b7280;
--danger: #d4503e;
--font: "Inter", system-ui, -apple-system, sans-serif;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(16, 50, 47, 0.05), 0 4px 14px rgba(16, 50, 47, 0.06);
--shadow-2: 0 16px 40px rgba(12, 122, 115, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
/* ── Layout ── */
.appts {
max-width: 640px;
margin: 0 auto;
padding: 32px 20px 56px;
display: flex;
flex-direction: column;
gap: 20px;
}
.appts-head {
display: flex;
flex-direction: column;
gap: 16px;
}
.appts-head h1 {
font-size: 1.6rem;
font-weight: 800;
letter-spacing: -0.02em;
}
/* ── Tabs ── */
.tabs {
display: inline-flex;
align-self: flex-start;
gap: 4px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px;
box-shadow: var(--shadow-1);
}
.tab {
border: none;
background: transparent;
border-radius: 999px;
padding: 8px 18px;
font: inherit;
font-weight: 600;
font-size: 0.9rem;
color: var(--muted);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: background 0.15s, color 0.15s;
}
.tab:hover {
color: var(--ink-2);
}
.tab.is-active {
background: var(--teal-d);
color: #fff;
}
.count {
display: inline-grid;
place-items: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 999px;
background: var(--teal-50);
color: var(--teal-d);
font-size: 0.74rem;
font-weight: 700;
}
.tab.is-active .count {
background: rgba(255, 255, 255, 0.22);
color: #fff;
}
/* ── Panels ── */
.panel {
display: flex;
flex-direction: column;
gap: 14px;
}
.panel[hidden] {
display: none;
}
/* ── Appointment card ── */
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px;
display: grid;
grid-template-columns: auto 1fr;
gap: 16px;
box-shadow: var(--shadow-1);
transition: opacity 0.2s, border-color 0.15s;
}
.card.is-past {
box-shadow: none;
background: #fafdfc;
}
.card.is-cancelled {
opacity: 0.7;
}
.date {
width: 58px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: var(--r-sm);
background: linear-gradient(150deg, var(--teal-50), #d9efec);
color: var(--teal-700);
padding: 10px 0;
}
.card.is-past .date {
background: #eef3f2;
color: var(--muted);
}
.day {
font-size: 1.5rem;
font-weight: 800;
line-height: 1;
}
.mon {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-top: 3px;
}
.info {
min-width: 0;
}
.info-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.info h2 {
font-size: 1.02rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.spec {
color: var(--muted);
font-size: 0.85rem;
margin-top: 1px;
}
.meta {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.time {
font-size: 0.9rem;
font-weight: 600;
color: var(--ink-2);
}
.mod {
font-size: 0.74rem;
font-weight: 700;
padding: 3px 9px;
border-radius: 999px;
}
.mod.video {
background: var(--coral-soft);
color: #c4503a;
}
.mod.person {
background: var(--teal-50);
color: var(--teal-d);
}
.loc {
font-size: 0.82rem;
color: var(--muted);
margin-top: 8px;
}
/* ── Badges ── */
.badge {
font-size: 0.72rem;
font-weight: 700;
padding: 4px 10px;
border-radius: 999px;
white-space: nowrap;
flex-shrink: 0;
}
.badge.ok {
background: rgba(47, 158, 111, 0.14);
color: var(--ok);
}
.badge.warn {
background: rgba(217, 138, 43, 0.16);
color: var(--warn);
}
.badge.cancelled {
background: rgba(212, 80, 62, 0.14);
color: var(--danger);
}
/* ── Actions ── */
.actions {
grid-column: 2;
display: flex;
gap: 8px;
margin-top: 14px;
flex-wrap: wrap;
}
.btn {
border: none;
border-radius: 10px;
padding: 9px 16px;
font: inherit;
font-weight: 600;
font-size: 0.84rem;
cursor: pointer;
transition: transform 0.12s, background 0.15s, border-color 0.15s;
}
.btn:active {
transform: translateY(1px);
}
.btn.ghost {
background: var(--white);
border: 1px solid var(--line-2);
color: var(--ink-2);
}
.btn.ghost:hover {
background: var(--teal-50);
border-color: var(--teal);
color: var(--teal-d);
}
.btn.danger {
background: var(--white);
border: 1px solid rgba(212, 80, 62, 0.4);
color: var(--danger);
}
.btn.danger:hover {
background: rgba(212, 80, 62, 0.08);
border-color: var(--danger);
}
.btn.link {
background: transparent;
color: var(--teal-d);
padding: 9px 4px;
text-decoration: underline;
}
.btn.link:hover {
color: var(--teal-700);
}
/* ── Inline cancel confirm ── */
.confirm {
grid-column: 2;
margin-top: 14px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
background: rgba(212, 80, 62, 0.07);
border: 1px solid rgba(212, 80, 62, 0.25);
border-radius: var(--r-sm);
padding: 12px 14px;
}
.confirm p {
font-size: 0.86rem;
font-weight: 600;
color: var(--ink-2);
flex: 1;
min-width: 160px;
}
.confirm-actions {
display: flex;
gap: 8px;
}
.btn.solid-danger {
background: var(--danger);
color: #fff;
}
.btn.solid-danger:hover {
background: #c2452f;
}
/* ── Toast ── */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
background: var(--ink);
color: #fff;
padding: 13px 20px;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-2);
z-index: 50;
max-width: 90vw;
}
@media (max-width: 520px) {
.card {
grid-template-columns: 1fr;
}
.date {
flex-direction: row;
width: auto;
gap: 8px;
align-self: flex-start;
padding: 6px 12px;
}
.mon {
margin-top: 0;
}
.actions,
.confirm {
grid-column: 1;
}
}// ── Toast ──────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2600);
}
// ── Tab switching ────────────────────────────────────────────────────────────
const tabs = document.querySelectorAll(".tab");
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
tabs.forEach((t) => {
const active = t === tab;
t.classList.toggle("is-active", active);
t.setAttribute("aria-selected", String(active));
document.getElementById(t.getAttribute("aria-controls")).hidden = !active;
});
});
});
// ── Update upcoming count from live DOM state ────────────────────────────────
function refreshUpcomingCount() {
const open = document.querySelectorAll('#panel-upcoming .card[data-state="upcoming"]').length;
document.getElementById("count-upcoming").textContent = open;
}
// ── Card actions ─────────────────────────────────────────────────────────────
function cancelCard(card) {
card.dataset.state = "cancelled";
card.classList.add("is-cancelled");
card.querySelector(".badge").className = "badge cancelled";
card.querySelector(".badge").textContent = "Cancelled";
card.querySelector(".confirm").remove();
refreshUpcomingCount();
showToast("Appointment cancelled — a confirmation has been sent.");
}
function askConfirm(card) {
if (card.querySelector(".confirm")) return;
const actions = card.querySelector(".actions");
actions.remove();
const box = document.createElement("div");
box.className = "confirm";
box.innerHTML =
"<p>Cancel this appointment?</p>" +
'<div class="confirm-actions">' +
'<button class="btn ghost" data-action="keep">Keep it</button>' +
'<button class="btn solid-danger" data-action="confirm">Yes, cancel</button>' +
"</div>";
card.appendChild(box);
}
function keepCard(card) {
card.querySelector(".confirm").remove();
const actions = document.createElement("div");
actions.className = "actions";
actions.innerHTML =
'<button class="btn ghost" data-action="reschedule">Reschedule</button>' +
'<button class="btn danger" data-action="cancel">Cancel</button>';
card.appendChild(actions);
}
document.querySelector(".appts").addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn || btn.classList.contains("tab")) return;
const card = btn.closest(".card");
const action = btn.dataset.action;
if (action === "cancel") askConfirm(card);
else if (action === "confirm") cancelCard(card);
else if (action === "keep") keepCard(card);
else if (action === "reschedule") showToast("Reschedule: pick a new slot from the calendar.");
else if (action === "summary") showToast("Opening your visit summary…");
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>My Appointments · Northbridge Clinic</title>
</head>
<body>
<main class="appts">
<header class="appts-head">
<h1>My appointments</h1>
<div class="tabs" role="tablist" aria-label="Appointments">
<button
class="tab is-active"
role="tab"
id="tab-upcoming"
aria-selected="true"
aria-controls="panel-upcoming"
data-tab="upcoming"
>
Upcoming <span class="count" id="count-upcoming">2</span>
</button>
<button
class="tab"
role="tab"
id="tab-past"
aria-selected="false"
aria-controls="panel-past"
data-tab="past"
>
Past <span class="count" id="count-past">2</span>
</button>
</div>
</header>
<section
class="panel"
role="tabpanel"
id="panel-upcoming"
aria-labelledby="tab-upcoming"
>
<article class="card" data-state="upcoming">
<div class="date">
<span class="day">09</span>
<span class="mon">Jun</span>
</div>
<div class="info">
<div class="info-top">
<h2>Dr. Lena Okafor</h2>
<span class="badge ok">Confirmed</span>
</div>
<p class="spec">Cardiology</p>
<p class="meta">
<span class="time">10:30 AM</span>
<span class="mod video">Video</span>
</p>
<p class="loc">Video consult · link sent 30 min before</p>
</div>
<div class="actions">
<button class="btn ghost" data-action="reschedule">Reschedule</button>
<button class="btn danger" data-action="cancel">Cancel</button>
</div>
</article>
<article class="card" data-state="upcoming">
<div class="date">
<span class="day">14</span>
<span class="mon">Jun</span>
</div>
<div class="info">
<div class="info-top">
<h2>Dr. Ravi Patel</h2>
<span class="badge warn">Check-in open</span>
</div>
<p class="spec">Primary care</p>
<p class="meta">
<span class="time">9:00 AM</span>
<span class="mod person">In-person</span>
</p>
<p class="loc">Northbridge Clinic · Suite 204, Floor 2</p>
</div>
<div class="actions">
<button class="btn ghost" data-action="reschedule">Reschedule</button>
<button class="btn danger" data-action="cancel">Cancel</button>
</div>
</article>
</section>
<section
class="panel"
role="tabpanel"
id="panel-past"
aria-labelledby="tab-past"
hidden
>
<article class="card is-past">
<div class="date">
<span class="day">21</span>
<span class="mon">May</span>
</div>
<div class="info">
<div class="info-top">
<h2>Dr. Lena Okafor</h2>
<span class="badge ok">Completed</span>
</div>
<p class="spec">Cardiology</p>
<p class="meta">
<span class="time">11:15 AM</span>
<span class="mod person">In-person</span>
</p>
<p class="loc">Northbridge Clinic · Suite 204, Floor 2</p>
</div>
<div class="actions">
<button class="btn link" data-action="summary">View summary</button>
</div>
</article>
<article class="card is-past">
<div class="date">
<span class="day">02</span>
<span class="mon">May</span>
</div>
<div class="info">
<div class="info-top">
<h2>Dr. Maya Bloom</h2>
<span class="badge cancelled">Cancelled</span>
</div>
<p class="spec">Dermatology</p>
<p class="meta">
<span class="time">3:45 PM</span>
<span class="mod video">Video</span>
</p>
<p class="loc">Video consult</p>
</div>
<div class="actions">
<button class="btn link" data-action="summary">View summary</button>
</div>
</article>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>My Appointments
A focused patient panel for managing visits. A segmented tab control switches between Upcoming and Past, each showing a live count. Upcoming cards lead with a teal date block, then the doctor, specialty and time, an in-person / video modality badge, a location line and status badges like Confirmed or Check-in open. Cancelling asks for an inline confirmation before marking the card cancelled, stripping its actions and decrementing the tab count. Past cards take a muted look with Completed or Cancelled badges and a view-summary action. All interactions are vanilla JS.
Illustrative UI only — not intended for real medical use.