Vet — Book an Appointment
A friendly multi-step veterinary booking flow to choose a pet or add a new one, pick a reason and an optional vet, select a date from a month calendar and an available time slot, confirm contact details, and review a validated confirmation summary with an add-to-calendar teaser.
MCP
Code
:root {
--teal: #179c8e;
--teal-d: #0f7f73;
--teal-50: #e6f5f2;
--coral: #ff8a5c;
--ink: #173a36;
--muted: #6b827e;
--bg: #f2f8f6;
--white: #ffffff;
--line: #d9e8e4;
--danger: #d6483b;
--radius: 18px;
--radius-sm: 12px;
--shadow: 0 14px 40px -22px rgba(15, 127, 115, 0.55);
--font: 'Inter', system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--font);
color: var(--ink);
background:
radial-gradient(1200px 520px at 100% -10%, var(--teal-50), transparent 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
padding: 32px 16px;
line-height: 1.5;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.booking {
max-width: 560px;
margin: 0 auto;
background: var(--white);
border: 1px solid var(--line);
border-radius: 26px;
box-shadow: var(--shadow);
overflow: hidden;
}
/* ---------- header ---------- */
.booking__head {
padding: 26px 28px 20px;
background: linear-gradient(160deg, var(--teal-50), var(--white));
border-bottom: 1px solid var(--line);
}
.brand {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.brand__paw {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 12px;
background: var(--white);
font-size: 20px;
box-shadow: 0 6px 16px -10px rgba(23, 156, 142, 0.7);
}
.brand__name {
margin: 0;
font-weight: 700;
font-size: 15px;
}
.brand__sub {
margin: 0;
font-size: 12px;
color: var(--muted);
}
.booking__head h1 {
margin: 0 0 4px;
font-size: 26px;
letter-spacing: -0.02em;
}
.booking__lede {
margin: 0 0 18px;
color: var(--muted);
font-size: 14px;
}
.steps {
display: flex;
gap: 6px;
list-style: none;
margin: 0;
padding: 0;
flex-wrap: wrap;
}
.steps__item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 600;
color: var(--muted);
background: var(--white);
border: 1px solid var(--line);
padding: 6px 10px 6px 6px;
border-radius: 999px;
transition: 0.2s;
}
.steps__item span {
display: grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bg);
color: var(--muted);
font-size: 11px;
}
.steps__item.is-active {
color: var(--teal-d);
border-color: var(--teal);
background: var(--teal-50);
}
.steps__item.is-active span {
background: var(--teal);
color: var(--white);
}
.steps__item.is-done span {
background: var(--coral);
color: var(--white);
}
/* ---------- steps body ---------- */
.step {
display: none;
padding: 24px 28px 28px;
animation: fade 0.32s ease;
}
.step.is-active {
display: block;
}
@keyframes fade {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.step__title {
margin: 0 0 18px;
font-size: 18px;
letter-spacing: -0.01em;
}
/* ---------- pet grid ---------- */
.petgrid {
border: 0;
margin: 0 0 18px;
padding: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.pet input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.pet__card {
display: flex;
flex-direction: column;
gap: 2px;
padding: 14px;
border: 1.5px solid var(--line);
border-radius: var(--radius);
background: var(--white);
cursor: pointer;
transition: 0.18s;
}
.pet__card:hover {
border-color: var(--teal);
transform: translateY(-2px);
}
.pet input:checked + .pet__card {
border-color: var(--teal);
background: var(--teal-50);
box-shadow: 0 0 0 3px rgba(23, 156, 142, 0.16);
}
.pet input:focus-visible + .pet__card {
outline: 3px solid var(--coral);
outline-offset: 2px;
}
.pet__avatar {
font-size: 26px;
line-height: 1;
}
.pet__avatar--plus {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 10px;
border: 1.5px dashed var(--teal);
color: var(--teal-d);
font-size: 20px;
}
.pet__name {
font-weight: 600;
font-size: 15px;
margin-top: 4px;
}
.pet__meta {
font-size: 12px;
color: var(--muted);
}
.newpet {
display: grid;
gap: 12px;
margin-bottom: 18px;
padding: 14px;
border-radius: var(--radius-sm);
background: var(--bg);
border: 1px solid var(--line);
}
/* ---------- fields ---------- */
.field {
margin-bottom: 16px;
}
.field label {
display: block;
font-size: 13px;
font-weight: 600;
margin-bottom: 6px;
}
.opt {
color: var(--muted);
font-weight: 400;
}
.field input[type="text"],
.field input[type="tel"],
.field input[type="email"],
.field select {
width: 100%;
font-family: inherit;
font-size: 14px;
color: var(--ink);
padding: 12px 13px;
border: 1.5px solid var(--line);
border-radius: var(--radius-sm);
background: var(--white);
transition: 0.16s;
}
.field input:focus,
.field select:focus {
outline: none;
border-color: var(--teal);
box-shadow: 0 0 0 3px rgba(23, 156, 142, 0.16);
}
.field.is-invalid input,
.field.is-invalid select {
border-color: var(--danger);
}
.error {
margin: 6px 0 0;
font-size: 12px;
color: var(--danger);
min-height: 0;
}
.field--check {
margin-top: 4px;
}
.check {
display: flex;
align-items: center;
gap: 9px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.check input {
width: 18px;
height: 18px;
accent-color: var(--teal);
}
/* ---------- calendar ---------- */
.calendar {
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 14px;
margin-bottom: 18px;
}
.calendar__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.calendar__month {
margin: 0;
font-weight: 700;
font-size: 15px;
}
.cal-nav {
width: 34px;
height: 34px;
border-radius: 10px;
border: 1px solid var(--line);
background: var(--white);
color: var(--teal-d);
font-size: 20px;
line-height: 1;
cursor: pointer;
transition: 0.15s;
}
.cal-nav:hover:not(:disabled) {
background: var(--teal-50);
border-color: var(--teal);
}
.cal-nav:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.calendar__dow {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 6px;
}
.calendar__dow span {
text-align: center;
font-size: 11px;
font-weight: 600;
color: var(--muted);
}
.calendar__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.day {
aspect-ratio: 1 / 1;
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
font-family: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink);
cursor: pointer;
transition: 0.14s;
}
.day:hover:not(:disabled):not(.is-selected) {
background: var(--teal-50);
}
.day:disabled {
color: #c2d3ce;
cursor: not-allowed;
}
.day.is-today {
border-color: var(--coral);
}
.day.is-selected {
background: var(--teal);
color: var(--white);
}
.day:focus-visible {
outline: 3px solid var(--coral);
outline-offset: 1px;
}
.day--empty {
visibility: hidden;
}
/* ---------- slots ---------- */
.slots {
margin-bottom: 18px;
}
.slots__label {
margin: 0 0 10px;
font-size: 13px;
font-weight: 600;
}
.slots__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.slot {
font-family: inherit;
font-size: 13px;
font-weight: 600;
padding: 11px 4px;
border: 1.5px solid var(--line);
border-radius: var(--radius-sm);
background: var(--white);
color: var(--ink);
cursor: pointer;
transition: 0.15s;
}
.slot:hover:not(:disabled):not(.is-selected) {
border-color: var(--teal);
background: var(--teal-50);
}
.slot:disabled {
opacity: 0.4;
cursor: not-allowed;
text-decoration: line-through;
}
.slot.is-selected {
background: var(--teal);
border-color: var(--teal);
color: var(--white);
}
.slot:focus-visible {
outline: 3px solid var(--coral);
outline-offset: 2px;
}
/* ---------- actions ---------- */
.actions {
display: flex;
gap: 10px;
margin-top: 8px;
}
.btn {
font-family: inherit;
font-size: 14px;
font-weight: 600;
padding: 13px 20px;
border-radius: 999px;
border: 1.5px solid transparent;
cursor: pointer;
transition: 0.16s;
}
.btn:focus-visible {
outline: 3px solid var(--coral);
outline-offset: 2px;
}
.btn--primary {
background: var(--teal);
color: var(--white);
margin-left: auto;
box-shadow: 0 10px 22px -12px rgba(15, 127, 115, 0.8);
}
.btn--primary:hover {
background: var(--teal-d);
transform: translateY(-1px);
}
.btn--ghost {
background: var(--white);
color: var(--ink);
border-color: var(--line);
}
.btn--ghost:hover {
background: var(--bg);
}
.btn--book {
background: var(--coral);
box-shadow: 0 10px 22px -12px rgba(255, 138, 92, 0.85);
}
.btn--book:hover {
background: #f5774a;
}
/* ---------- summary ---------- */
.summary {
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 16px;
}
.sum-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 13px 16px;
border-bottom: 1px solid var(--line);
}
.sum-row:last-child {
border-bottom: 0;
}
.sum-row__ico {
font-size: 17px;
width: 22px;
text-align: center;
}
.sum-row__k {
font-size: 12px;
color: var(--muted);
margin: 0;
}
.sum-row__v {
font-size: 14px;
font-weight: 600;
margin: 1px 0 0;
}
.sum-badge {
margin-left: auto;
align-self: center;
font-size: 11px;
font-weight: 700;
padding: 4px 10px;
border-radius: 999px;
background: var(--teal-50);
color: var(--teal-d);
}
.addcal {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border: 1px dashed var(--teal);
border-radius: var(--radius);
background: var(--teal-50);
margin-bottom: 18px;
}
.addcal__ico {
font-size: 22px;
}
.addcal__t {
margin: 0;
font-weight: 600;
font-size: 14px;
}
.addcal__s {
margin: 1px 0 0;
font-size: 12px;
color: var(--muted);
}
/* ---------- success ---------- */
.success {
padding: 44px 28px 40px;
text-align: center;
animation: fade 0.32s ease;
}
.success__mark {
width: 72px;
height: 72px;
margin: 0 auto 16px;
display: grid;
place-items: center;
font-size: 34px;
border-radius: 50%;
background: var(--teal-50);
border: 2px solid var(--teal);
animation: pop 0.45s cubic-bezier(0.2, 1.4, 0.5, 1) both;
}
@keyframes pop {
from { transform: scale(0.4); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.success h2 {
margin: 0 0 6px;
font-size: 24px;
}
.success p {
margin: 0 auto 8px;
max-width: 360px;
color: var(--muted);
font-size: 14px;
}
.success__ref {
font-size: 13px;
}
.success__ref strong {
color: var(--teal-d);
}
.success .btn {
margin-top: 12px;
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
body {
padding: 0;
}
.booking {
border-radius: 0;
border-left: 0;
border-right: 0;
min-height: 100vh;
}
.booking__head,
.step,
.success {
padding-left: 18px;
padding-right: 18px;
}
.booking__head h1 {
font-size: 22px;
}
.steps__item {
font-size: 0;
gap: 0;
padding: 5px;
}
.steps__item span {
font-size: 11px;
}
.petgrid {
grid-template-columns: 1fr;
}
.slots__grid {
grid-template-columns: repeat(2, 1fr);
}
.actions {
flex-wrap: wrap;
}
.btn--primary {
width: 100%;
}
}(function () {
'use strict';
var form = document.getElementById('bookingForm');
if (!form) return;
var steps = Array.prototype.slice.call(form.querySelectorAll('.step'));
var dots = Array.prototype.slice.call(document.querySelectorAll('[data-step-dot]'));
var successEl = document.getElementById('success');
var state = {
step: 1,
pet: 'Luna',
petMeta: 'Cat · 4 yrs',
reason: '',
vet: '',
date: null, // Date object
time: ''
};
// ---------- step navigation ----------
function showStep(n) {
state.step = n;
steps.forEach(function (s) {
var match = Number(s.getAttribute('data-step')) === n;
s.classList.toggle('is-active', match);
s.hidden = !match;
});
dots.forEach(function (d) {
var dn = Number(d.getAttribute('data-step-dot'));
d.classList.toggle('is-active', dn === n);
d.classList.toggle('is-done', dn < n);
});
if (n === 4) renderSummary();
var heading = steps[n - 1] && steps[n - 1].querySelector('.step__title');
if (heading) heading.setAttribute('tabindex', '-1'), heading.focus();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// ---------- validation helpers ----------
function setError(name, msg) {
var el = form.querySelector('[data-error="' + name + '"]');
if (el) el.textContent = msg || '';
var field = el && el.closest('.field');
if (field) field.classList.toggle('is-invalid', !!msg);
}
function clearErrors() {
form.querySelectorAll('.error').forEach(function (e) { e.textContent = ''; });
form.querySelectorAll('.is-invalid').forEach(function (f) { f.classList.remove('is-invalid'); });
}
function validateStep(n) {
var ok = true;
if (n === 1) {
if (state.pet === '__new') {
var nm = form.newPetName.value.trim();
if (!nm) { setError('newPetName', 'Please name your pet.'); ok = false; }
else setError('newPetName', '');
}
if (!form.reason.value) { setError('reason', 'Pick a reason for the visit.'); ok = false; }
else setError('reason', '');
}
if (n === 2) {
if (!state.date) { setError('date', 'Choose a date.'); ok = false; }
else setError('date', '');
if (state.date && !state.time) { setError('time', 'Choose a time slot.'); ok = false; }
else if (state.time) setError('time', '');
}
if (n === 3) {
var owner = form.owner.value.trim();
var phone = form.phone.value.trim();
var email = form.email.value.trim();
if (!owner) { setError('owner', 'Your name is required.'); ok = false; } else setError('owner', '');
if (phone.replace(/\D/g, '').length < 7) { setError('phone', 'Enter a valid phone number.'); ok = false; } else setError('phone', '');
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { setError('email', 'Enter a valid email.'); ok = false; } else setError('email', '');
}
return ok;
}
// ---------- pet selection ----------
form.querySelectorAll('input[name="pet"]').forEach(function (r) {
r.addEventListener('change', function () {
state.pet = r.value;
state.petMeta = r.getAttribute('data-kind') || '';
document.getElementById('newPet').hidden = r.value !== '__new';
if (r.value !== '__new') setError('newPetName', '');
});
});
form.reason.addEventListener('change', function () { state.reason = form.reason.value; });
form.vet.addEventListener('change', function () { state.vet = form.vet.value; });
// ---------- calendar ----------
var calGrid = document.getElementById('calGrid');
var calMonth = document.getElementById('calMonth');
var prevBtn = document.querySelector('[data-cal-prev]');
var nextBtn = document.querySelector('[data-cal-next]');
var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
var today = new Date();
today.setHours(0, 0, 0, 0);
var view = new Date(today.getFullYear(), today.getMonth(), 1);
var minView = new Date(today.getFullYear(), today.getMonth(), 1);
function sameDay(a, b) {
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}
function renderCalendar() {
calMonth.textContent = MONTHS[view.getMonth()] + ' ' + view.getFullYear();
prevBtn.disabled = view.getFullYear() === minView.getFullYear() && view.getMonth() === minView.getMonth();
calGrid.innerHTML = '';
var first = new Date(view.getFullYear(), view.getMonth(), 1);
var startDow = first.getDay();
var daysInMonth = new Date(view.getFullYear(), view.getMonth() + 1, 0).getDate();
for (var i = 0; i < startDow; i++) {
var blank = document.createElement('span');
blank.className = 'day day--empty';
calGrid.appendChild(blank);
}
for (var d = 1; d <= daysInMonth; d++) {
var cellDate = new Date(view.getFullYear(), view.getMonth(), d);
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'day';
btn.textContent = String(d);
btn.setAttribute('role', 'gridcell');
var isPast = cellDate < today;
var isSunday = cellDate.getDay() === 0; // clinic closed Sundays
if (isPast || isSunday) {
btn.disabled = true;
btn.setAttribute('aria-disabled', 'true');
}
if (sameDay(cellDate, today)) btn.classList.add('is-today');
if (state.date && sameDay(cellDate, state.date)) {
btn.classList.add('is-selected');
btn.setAttribute('aria-selected', 'true');
}
(function (cd) {
btn.addEventListener('click', function () {
state.date = cd;
state.time = '';
setError('date', '');
renderCalendar();
renderSlots();
});
})(cellDate);
calGrid.appendChild(btn);
}
}
prevBtn.addEventListener('click', function () {
view = new Date(view.getFullYear(), view.getMonth() - 1, 1);
renderCalendar();
});
nextBtn.addEventListener('click', function () {
view = new Date(view.getFullYear(), view.getMonth() + 1, 1);
renderCalendar();
});
// ---------- time slots ----------
var slotsWrap = document.getElementById('slotsWrap');
var slotsGrid = document.getElementById('slotsGrid');
var slotsLabel = document.getElementById('slotsLabel');
var BASE_SLOTS = ['9:00', '9:40', '10:20', '11:00', '11:40',
'13:20', '14:00', '14:40', '15:20', '16:00', '16:40'];
function fmtDate(d) {
return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
}
function renderSlots() {
if (!state.date) { slotsWrap.hidden = true; return; }
slotsWrap.hidden = false;
slotsLabel.textContent = 'Available times — ' + fmtDate(state.date);
slotsGrid.innerHTML = '';
// deterministically mark a couple of slots taken, based on the date
var seed = state.date.getDate();
BASE_SLOTS.forEach(function (t, idx) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'slot';
btn.textContent = t;
var taken = (idx + seed) % 4 === 0;
if (taken) {
btn.disabled = true;
btn.title = 'Fully booked';
}
if (state.time === t) btn.classList.add('is-selected');
btn.addEventListener('click', function () {
state.time = t;
setError('time', '');
slotsGrid.querySelectorAll('.slot').forEach(function (s) { s.classList.remove('is-selected'); });
btn.classList.add('is-selected');
});
slotsGrid.appendChild(btn);
});
}
// ---------- summary ----------
function petLabel() {
if (state.pet === '__new') {
var nm = form.newPetName.value.trim() || 'New pet';
var kind = form.newPetKind.value.trim();
return { name: nm, meta: kind || 'New patient' };
}
return { name: state.pet, meta: state.petMeta };
}
function renderSummary() {
var pet = petLabel();
var rows = [
{ ico: '🐾', k: 'Pet', v: pet.name + ' · ' + pet.meta, badge: 'New' },
{ ico: '🩺', k: 'Reason', v: form.reason.value || '—' },
{ ico: '👩⚕️', k: 'Vet', v: form.vet.value || 'No preference' },
{ ico: '📆', k: 'Date', v: state.date ? fmtDate(state.date) : '—' },
{ ico: '⏰', k: 'Time', v: state.time || '—' },
{ ico: '📞', k: 'Contact', v: form.owner.value.trim() + ' · ' + form.phone.value.trim() }
];
var html = rows.map(function (r) {
var badge = (r.badge && state.pet === '__new') ? '<span class="sum-badge">' + r.badge + '</span>' : '';
return '<div class="sum-row"><span class="sum-row__ico">' + r.ico + '</span>' +
'<div><p class="sum-row__k">' + r.k + '</p><p class="sum-row__v">' + escapeHtml(r.v) + '</p></div>' +
badge + '</div>';
}).join('');
document.getElementById('summary').innerHTML = html;
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
// ---------- buttons ----------
form.querySelectorAll('[data-next]').forEach(function (b) {
b.addEventListener('click', function () {
if (validateStep(state.step)) showStep(state.step + 1);
});
});
form.querySelectorAll('[data-prev]').forEach(function (b) {
b.addEventListener('click', function () { showStep(state.step - 1); });
});
// ---------- submit ----------
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!validateStep(3)) { showStep(3); return; }
var pet = petLabel();
var ref = 'PW-' + (1000 + Math.floor(Math.random() * 9000));
document.getElementById('successRef').textContent = ref;
document.getElementById('successMsg').textContent =
pet.name + "'s " + (form.reason.value || 'visit').toLowerCase() +
' is booked for ' + fmtDate(state.date) + ' at ' + state.time + '.';
form.hidden = true;
document.querySelector('.booking__head .steps').style.display = 'none';
successEl.hidden = false;
successEl.querySelector('h2').setAttribute('tabindex', '-1');
successEl.querySelector('h2').focus();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
document.getElementById('bookAnother').addEventListener('click', function () {
form.reset();
clearErrors();
state.pet = 'Luna';
state.petMeta = 'Cat · 4 yrs';
state.reason = '';
state.vet = '';
state.date = null;
state.time = '';
document.getElementById('newPet').hidden = true;
slotsWrap.hidden = true;
view = new Date(minView.getFullYear(), minView.getMonth(), 1);
renderCalendar();
successEl.hidden = true;
form.hidden = false;
document.querySelector('.booking__head .steps').style.display = '';
showStep(1);
});
// ---------- init ----------
renderCalendar();
showStep(1);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Book an Appointment — Paws & Whiskers Vet</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&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="booking" aria-labelledby="booking-title">
<header class="booking__head">
<div class="brand">
<span class="brand__paw" aria-hidden="true">🐾</span>
<div>
<p class="brand__name">Paws & Whiskers</p>
<p class="brand__sub">Veterinary Clinic</p>
</div>
</div>
<h1 id="booking-title">Book an appointment</h1>
<p class="booking__lede">A few quick steps and your furry friend is on the schedule.</p>
<ol class="steps" aria-label="Booking progress">
<li class="steps__item is-active" data-step-dot="1"><span>1</span>Pet & reason</li>
<li class="steps__item" data-step-dot="2"><span>2</span>Date & time</li>
<li class="steps__item" data-step-dot="3"><span>3</span>Your details</li>
<li class="steps__item" data-step-dot="4"><span>4</span>Confirm</li>
</ol>
</header>
<form id="bookingForm" novalidate>
<!-- STEP 1 -->
<section class="step is-active" data-step="1" aria-labelledby="s1-title">
<h2 id="s1-title" class="step__title">Who's visiting us?</h2>
<fieldset class="petgrid" aria-describedby="s1-title">
<legend class="sr-only">Choose a pet</legend>
<label class="pet">
<input type="radio" name="pet" value="Luna" data-kind="Cat · 4 yrs" checked />
<span class="pet__card">
<span class="pet__avatar" aria-hidden="true">🐱</span>
<span class="pet__name">Luna</span>
<span class="pet__meta">Cat · 4 yrs</span>
</span>
</label>
<label class="pet">
<input type="radio" name="pet" value="Biscuit" data-kind="Corgi · 2 yrs" />
<span class="pet__card">
<span class="pet__avatar" aria-hidden="true">🐶</span>
<span class="pet__name">Biscuit</span>
<span class="pet__meta">Corgi · 2 yrs</span>
</span>
</label>
<label class="pet">
<input type="radio" name="pet" value="Mango" data-kind="Parrot · 6 yrs" />
<span class="pet__card">
<span class="pet__avatar" aria-hidden="true">🦜</span>
<span class="pet__name">Mango</span>
<span class="pet__meta">Parrot · 6 yrs</span>
</span>
</label>
<label class="pet pet--add">
<input type="radio" name="pet" value="__new" data-kind="New pet" />
<span class="pet__card">
<span class="pet__avatar pet__avatar--plus" aria-hidden="true">+</span>
<span class="pet__name">Add a pet</span>
<span class="pet__meta">New patient</span>
</span>
</label>
</fieldset>
<div class="newpet" id="newPet" hidden>
<div class="field">
<label for="newPetName">Pet's name</label>
<input type="text" id="newPetName" name="newPetName" placeholder="e.g. Pepper" autocomplete="off" />
<p class="error" data-error="newPetName"></p>
</div>
<div class="field">
<label for="newPetKind">Species & age</label>
<input type="text" id="newPetKind" name="newPetKind" placeholder="e.g. Rabbit · 1 yr" autocomplete="off" />
</div>
</div>
<div class="field">
<label for="reason">Reason for visit</label>
<select id="reason" name="reason">
<option value="">Choose a reason…</option>
<option value="Wellness checkup">Wellness checkup</option>
<option value="Vaccination">Vaccination</option>
<option value="Dental cleaning">Dental cleaning</option>
<option value="Skin or coat concern">Skin or coat concern</option>
<option value="Injury or limping">Injury or limping</option>
<option value="Not eating / unwell">Not eating / unwell</option>
<option value="Other">Other</option>
</select>
<p class="error" data-error="reason"></p>
</div>
<div class="field">
<label for="vet">Preferred vet <span class="opt">(optional)</span></label>
<select id="vet" name="vet">
<option value="">No preference</option>
<option value="Dr. Amara Okafor">Dr. Amara Okafor — General</option>
<option value="Dr. Liam Chen">Dr. Liam Chen — Surgery</option>
<option value="Dr. Sofia Reyes">Dr. Sofia Reyes — Exotics & birds</option>
</select>
</div>
<div class="actions">
<button type="button" class="btn btn--primary" data-next>Continue</button>
</div>
</section>
<!-- STEP 2 -->
<section class="step" data-step="2" aria-labelledby="s2-title" hidden>
<h2 id="s2-title" class="step__title">Pick a date & time</h2>
<div class="calendar" role="group" aria-label="Choose a date">
<div class="calendar__head">
<button type="button" class="cal-nav" data-cal-prev aria-label="Previous month">‹</button>
<p class="calendar__month" id="calMonth" aria-live="polite">Month</p>
<button type="button" class="cal-nav" data-cal-next aria-label="Next month">›</button>
</div>
<div class="calendar__dow" aria-hidden="true">
<span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span>
</div>
<div class="calendar__grid" id="calGrid" role="grid"></div>
<p class="error" data-error="date"></p>
</div>
<div class="slots" id="slotsWrap" hidden>
<p class="slots__label" id="slotsLabel">Available times</p>
<div class="slots__grid" id="slotsGrid" role="group" aria-labelledby="slotsLabel"></div>
<p class="error" data-error="time"></p>
</div>
<div class="actions">
<button type="button" class="btn btn--ghost" data-prev>Back</button>
<button type="button" class="btn btn--primary" data-next>Continue</button>
</div>
</section>
<!-- STEP 3 -->
<section class="step" data-step="3" aria-labelledby="s3-title" hidden>
<h2 id="s3-title" class="step__title">Your contact details</h2>
<div class="field">
<label for="owner">Your name</label>
<input type="text" id="owner" name="owner" placeholder="e.g. Jordan Mills" autocomplete="name" />
<p class="error" data-error="owner"></p>
</div>
<div class="field">
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone" placeholder="(555) 204-8819" autocomplete="tel" />
<p class="error" data-error="phone"></p>
</div>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="[email protected]" autocomplete="email" />
<p class="error" data-error="email"></p>
</div>
<div class="field field--check">
<label class="check">
<input type="checkbox" id="reminders" name="reminders" checked />
<span>Text me a reminder the day before</span>
</label>
</div>
<div class="actions">
<button type="button" class="btn btn--ghost" data-prev>Back</button>
<button type="button" class="btn btn--primary" data-next>Review</button>
</div>
</section>
<!-- STEP 4 -->
<section class="step" data-step="4" aria-labelledby="s4-title" hidden>
<h2 id="s4-title" class="step__title">Confirm your visit</h2>
<div class="summary" id="summary" aria-live="polite"></div>
<div class="addcal" aria-hidden="true">
<span class="addcal__ico">📅</span>
<div>
<p class="addcal__t">Add to calendar</p>
<p class="addcal__s">We'll send a Google & Apple Calendar invite after you book.</p>
</div>
</div>
<div class="actions">
<button type="button" class="btn btn--ghost" data-prev>Back</button>
<button type="submit" class="btn btn--primary btn--book">Confirm booking</button>
</div>
</section>
</form>
<!-- SUCCESS -->
<section class="success" id="success" hidden aria-live="assertive">
<div class="success__mark" aria-hidden="true">🐾</div>
<h2>You're all set!</h2>
<p id="successMsg">Your appointment is confirmed.</p>
<p class="success__ref">Booking ref <strong id="successRef">PW-0000</strong></p>
<button type="button" class="btn btn--primary" id="bookAnother">Book another</button>
</section>
</main>
<script src="script.js"></script>
</body>
</html>Book an Appointment
A warm, approachable veterinary booking page built as a single guided flow. Pet owners pick one of their pets (Luna the cat, Biscuit the corgi, Mango the parrot) or add a new one inline, choose a reason for the visit, optionally request a specific vet, then move into scheduling.
The scheduler renders a real month calendar with disabled past days and weekend handling, and reveals a grid of available time slots once a date is selected. A contact step collects the owner’s name, phone and email with live validation, and the final summary card recaps everything with an add-to-calendar teaser and a success state.
Everything runs on vanilla JS — date math, slot generation, step navigation, validation and the confirmation render are all hand-rolled with no libraries.
Illustrative UI only — not for real veterinary use.