Vet — Contact & Location
A friendly veterinary contact section pairing an appointment-request form with live field validation against an info card showing address, phone, weekly hours with an auto open or closed status, and a stylized map placeholder with a clinic marker.
MCP
Code
:root {
--teal: #179c8e;
--teal-d: #0f7f73;
--teal-50: #e6f5f2;
--coral: #ff8a5c;
--ink: #173a36;
--muted: #6b827e;
--bg: #f2f8f6;
--white: #ffffff;
--line: #d9ece8;
--danger: #d9534f;
--radius: 20px;
--radius-sm: 12px;
--shadow: 0 12px 30px -12px rgba(15, 127, 115, 0.25);
--font: 'Inter', system-ui, -apple-system, sans-serif;
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: var(--font);
color: var(--ink);
background: var(--bg);
line-height: 1.5;
padding: 2.5rem 1.25rem;
}
.contact {
max-width: 980px;
margin: 0 auto;
}
.contact__intro {
text-align: center;
margin-bottom: 2rem;
}
.eyebrow {
margin: 0;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--teal-d);
}
.contact__intro h1 {
margin: 0.35rem 0 0.5rem;
font-size: 1.9rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.lede {
margin: 0 auto;
max-width: 38ch;
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 1.25rem;
align-items: start;
}
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1.6rem;
box-shadow: var(--shadow);
}
.card__title {
display: flex;
align-items: center;
gap: 0.55rem;
margin: 0 0 1.2rem;
font-size: 1.15rem;
font-weight: 700;
}
.card__icon {
display: inline-grid;
place-items: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: var(--teal-50);
font-size: 1rem;
}
/* ---------- Form ---------- */
.field {
margin-bottom: 1rem;
}
.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.9rem;
}
label {
display: block;
margin-bottom: 0.35rem;
font-size: 0.85rem;
font-weight: 600;
}
.optional {
font-weight: 400;
color: var(--muted);
}
input,
select,
textarea {
width: 100%;
font: inherit;
color: var(--ink);
background: var(--bg);
border: 1.5px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.7rem 0.85rem;
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
textarea {
resize: vertical;
}
input::placeholder,
textarea::placeholder {
color: #9fb3ae;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
background: var(--white);
border-color: var(--teal);
box-shadow: 0 0 0 3px rgba(23, 156, 142, 0.18);
}
.field.invalid input,
.field.invalid select,
.field.invalid textarea {
border-color: var(--danger);
}
.field.invalid input:focus,
.field.invalid select:focus {
box-shadow: 0 0 0 3px rgba(217, 83, 79, 0.18);
}
.error {
margin: 0.3rem 0 0;
min-height: 1rem;
font-size: 0.78rem;
color: var(--danger);
}
.btn {
width: 100%;
margin-top: 0.4rem;
font: inherit;
font-weight: 600;
color: var(--white);
background: var(--teal);
border: none;
border-radius: 999px;
padding: 0.85rem 1.2rem;
cursor: pointer;
transition: background 0.15s ease, transform 0.08s ease;
}
.btn:hover {
background: var(--teal-d);
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 3px solid rgba(23, 156, 142, 0.4);
outline-offset: 2px;
}
.form-status {
margin: 0.9rem 0 0;
font-size: 0.88rem;
font-weight: 600;
text-align: center;
min-height: 1.2rem;
}
.form-status.success {
color: var(--teal-d);
}
.form-status.error {
color: var(--danger);
}
/* ---------- Side / info ---------- */
.side {
display: grid;
gap: 1.25rem;
}
.info-card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 1.2rem;
}
.info-card__head .card__title {
margin: 0;
}
.status-badge {
flex-shrink: 0;
font-size: 0.74rem;
font-weight: 600;
padding: 0.3rem 0.65rem;
border-radius: 999px;
background: #e6f0ee;
color: var(--muted);
white-space: nowrap;
}
.status-badge.open {
background: var(--teal-50);
color: var(--teal-d);
}
.status-badge.closed {
background: #ffe7dc;
color: #c2502a;
}
.info-list {
list-style: none;
margin: 0 0 1.4rem;
padding: 0;
display: grid;
gap: 0.8rem;
}
.info-list li {
display: flex;
gap: 0.7rem;
align-items: flex-start;
font-size: 0.92rem;
}
.info-list__icon {
flex-shrink: 0;
}
.info-list a {
color: var(--teal-d);
font-weight: 600;
text-decoration: none;
}
.info-list a:hover {
text-decoration: underline;
}
.hours-title {
margin: 0 0 0.6rem;
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.hours {
list-style: none;
margin: 0;
padding: 0;
}
.hours li {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
padding: 0.4rem 0.55rem;
border-radius: 8px;
}
.hours li.today {
background: var(--teal-50);
font-weight: 600;
}
.hours .day {
color: var(--ink);
}
.hours .time {
color: var(--muted);
}
.hours li.today .time {
color: var(--teal-d);
}
.hours .time.closed {
color: var(--coral);
}
/* ---------- Map ---------- */
.map-card {
padding: 0;
overflow: hidden;
}
.map {
position: relative;
line-height: 0;
}
.map__svg {
width: 100%;
height: auto;
display: block;
}
.map__pin {
position: absolute;
top: 50%;
left: 44%;
transform: translate(-50%, -100%);
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.3;
}
.map__dot {
width: 16px;
height: 16px;
border-radius: 50% 50% 50% 0;
background: var(--coral);
transform: rotate(-45deg);
box-shadow: 0 4px 8px rgba(255, 138, 92, 0.5);
animation: pin-bounce 2s ease-in-out infinite;
}
.map__label {
margin-top: 0.45rem;
font-size: 0.72rem;
font-weight: 600;
color: var(--ink);
background: var(--white);
padding: 0.18rem 0.5rem;
border-radius: 999px;
box-shadow: 0 2px 6px rgba(15, 127, 115, 0.15);
white-space: nowrap;
}
@keyframes pin-bounce {
0%, 100% { transform: rotate(-45deg) translateY(0); }
50% { transform: rotate(-45deg) translateY(-4px); }
}
.map__link {
display: block;
text-align: center;
padding: 0.9rem;
font-size: 0.88rem;
font-weight: 600;
color: var(--teal-d);
text-decoration: none;
border-top: 1px solid var(--line);
}
.map__link:hover {
background: var(--teal-50);
}
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
body {
padding: 1.5rem 0.9rem;
}
.contact__intro h1 {
font-size: 1.5rem;
}
.card {
padding: 1.25rem;
}
.field-row {
grid-template-columns: 1fr;
gap: 0;
}
.info-card__head {
flex-direction: column;
align-items: flex-start;
}
}
@media (prefers-reduced-motion: reduce) {
.map__dot {
animation: none;
}
}(function () {
'use strict';
/* ---------- Opening hours + open/closed status ---------- */
// 0 = Sunday … 6 = Saturday. open/close in 24h minutes; null = closed.
var SCHEDULE = [
{ label: 'Sunday', open: null, close: null },
{ label: 'Monday', open: 8 * 60, close: 18 * 60 },
{ label: 'Tuesday', open: 8 * 60, close: 18 * 60 },
{ label: 'Wednesday', open: 8 * 60, close: 18 * 60 },
{ label: 'Thursday', open: 8 * 60, close: 19 * 60 },
{ label: 'Friday', open: 8 * 60, close: 18 * 60 },
{ label: 'Saturday', open: 9 * 60, close: 14 * 60 }
];
function fmt(mins) {
var h = Math.floor(mins / 60);
var m = mins % 60;
var ampm = h >= 12 ? 'PM' : 'AM';
var h12 = h % 12 || 12;
return h12 + (m ? ':' + String(m).padStart(2, '0') : '') + ' ' + ampm;
}
function renderHours() {
var list = document.getElementById('hours');
var badge = document.getElementById('status-badge');
if (!list || !badge) return;
var now = new Date();
var todayIdx = now.getDay();
var nowMins = now.getHours() * 60 + now.getMinutes();
list.innerHTML = '';
SCHEDULE.forEach(function (d, i) {
var li = document.createElement('li');
if (i === todayIdx) li.className = 'today';
var day = document.createElement('span');
day.className = 'day';
day.textContent = d.label;
var time = document.createElement('span');
time.className = 'time';
if (d.open === null) {
time.textContent = 'Closed';
time.classList.add('closed');
} else {
time.textContent = fmt(d.open) + ' – ' + fmt(d.close);
}
li.appendChild(day);
li.appendChild(time);
list.appendChild(li);
});
var today = SCHEDULE[todayIdx];
var isOpen = today.open !== null && nowMins >= today.open && nowMins < today.close;
badge.classList.remove('open', 'closed');
if (isOpen) {
badge.classList.add('open');
badge.textContent = '● Open now · until ' + fmt(today.close);
} else {
badge.classList.add('closed');
// find next opening day
var next = null;
for (var step = 0; step < 7; step++) {
var idx = (todayIdx + step) % 7;
var d = SCHEDULE[idx];
if (d.open === null) continue;
if (step === 0 && nowMins < d.open) { next = 'today at ' + fmt(d.open); break; }
if (step > 0) { next = (step === 1 ? 'tomorrow' : d.label) + ' at ' + fmt(d.open); break; }
}
badge.textContent = '● Closed' + (next ? ' · opens ' + next : '');
}
}
/* ---------- Form validation ---------- */
var form = document.getElementById('vet-form');
var statusEl = document.getElementById('form-status');
var validators = {
owner: function (v) { return v.trim().length >= 2 || 'Please enter your name.'; },
pet: function (v) { return v.trim().length >= 1 || "Please enter your pet's name."; },
phone: function (v) {
var digits = v.replace(/\D/g, '');
return digits.length >= 7 || 'Enter a valid phone number.';
},
reason: function (v) { return v !== '' || 'Please choose a reason.'; }
};
function validateField(name) {
var input = form.elements[name];
if (!input) return true;
var field = input.closest('.field');
var errEl = form.querySelector('[data-error-for="' + name + '"]');
var result = validators[name](input.value);
if (result === true) {
field.classList.remove('invalid');
if (errEl) errEl.textContent = '';
input.removeAttribute('aria-invalid');
return true;
}
field.classList.add('invalid');
if (errEl) errEl.textContent = result;
input.setAttribute('aria-invalid', 'true');
return false;
}
if (form) {
Object.keys(validators).forEach(function (name) {
var input = form.elements[name];
if (!input) return;
input.addEventListener('blur', function () { validateField(name); });
input.addEventListener('input', function () {
if (input.closest('.field').classList.contains('invalid')) validateField(name);
});
});
form.addEventListener('submit', function (e) {
e.preventDefault();
statusEl.className = 'form-status';
statusEl.textContent = '';
var ok = Object.keys(validators).every(function (name) {
return validateField(name);
});
if (!ok) {
statusEl.classList.add('error');
statusEl.textContent = 'Please fix the highlighted fields.';
var firstInvalid = form.querySelector('.field.invalid input, .field.invalid select');
if (firstInvalid) firstInvalid.focus();
return;
}
var pet = form.elements.pet.value.trim();
statusEl.classList.add('success');
statusEl.textContent = 'Thanks! We\'ll confirm ' + pet + "'s appointment within one business day.";
form.reset();
form.querySelectorAll('.field.invalid').forEach(function (f) {
f.classList.remove('invalid');
});
});
}
renderHours();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vet — Contact & Location</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="contact" aria-labelledby="contact-title">
<header class="contact__intro">
<p class="eyebrow">Paws & Whiskers Veterinary</p>
<h1 id="contact-title">Book a visit or drop by</h1>
<p class="lede">Tell us about your companion and we'll confirm an appointment within one business day.</p>
</header>
<div class="grid">
<!-- Appointment request form -->
<section class="card form-card" aria-labelledby="form-title">
<h2 id="form-title" class="card__title">
<span class="card__icon" aria-hidden="true">🐾</span> Request an appointment
</h2>
<form id="vet-form" novalidate>
<div class="field">
<label for="owner">Your name</label>
<input type="text" id="owner" name="owner" autocomplete="name" placeholder="e.g. Marina Delgado" required />
<p class="error" data-error-for="owner"></p>
</div>
<div class="field-row">
<div class="field">
<label for="pet">Pet name</label>
<input type="text" id="pet" name="pet" placeholder="e.g. Biscuit" required />
<p class="error" data-error-for="pet"></p>
</div>
<div class="field">
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone" autocomplete="tel" placeholder="(555) 204-1180" required />
<p class="error" data-error-for="phone"></p>
</div>
</div>
<div class="field">
<label for="reason">Reason for visit</label>
<select id="reason" name="reason" required>
<option value="" disabled selected>Choose a reason…</option>
<option value="wellness">Wellness check-up</option>
<option value="vaccination">Vaccination</option>
<option value="dental">Dental cleaning</option>
<option value="injury">Injury or limping</option>
<option value="skin">Skin or itching</option>
<option value="other">Something else</option>
</select>
<p class="error" data-error-for="reason"></p>
</div>
<div class="field">
<label for="message">Message <span class="optional">(optional)</span></label>
<textarea id="message" name="message" rows="3" placeholder="Biscuit has been scratching her left ear since the weekend."></textarea>
</div>
<button type="submit" class="btn">Request appointment</button>
<p class="form-status" id="form-status" role="status" aria-live="polite"></p>
</form>
</section>
<!-- Info + map -->
<aside class="side">
<section class="card info-card" aria-labelledby="info-title">
<div class="info-card__head">
<h2 id="info-title" class="card__title">
<span class="card__icon" aria-hidden="true">🏥</span> Clinic info
</h2>
<span class="status-badge" id="status-badge" aria-live="polite">Checking…</span>
</div>
<ul class="info-list">
<li>
<span class="info-list__icon" aria-hidden="true">📍</span>
<span>148 Maple Hollow Rd, Suite 3<br />Brookfield, OR 97402</span>
</li>
<li>
<span class="info-list__icon" aria-hidden="true">📞</span>
<a href="tel:+15552041180">(555) 204-1180</a>
</li>
<li>
<span class="info-list__icon" aria-hidden="true">✉️</span>
<a href="mailto:[email protected]">[email protected]</a>
</li>
</ul>
<h3 class="hours-title">Opening hours</h3>
<ul class="hours" id="hours"></ul>
</section>
<section class="card map-card" aria-label="Clinic location map">
<div class="map">
<svg class="map__svg" viewBox="0 0 400 220" role="img" aria-label="Stylized map showing the clinic at 148 Maple Hollow Rd">
<rect width="400" height="220" fill="#e6f5f2" />
<path d="M0 60 H400 M0 150 H400 M120 0 V220 M280 0 V220" stroke="#cfe8e3" stroke-width="10" />
<path d="M0 105 H400" stroke="#bfe1da" stroke-width="4" stroke-dasharray="14 10" />
<circle cx="60" cy="40" r="22" fill="#d6efe9" />
<circle cx="340" cy="180" r="30" fill="#d6efe9" />
<rect x="150" y="80" width="40" height="40" rx="6" fill="#ffffff" stroke="#cfe8e3" stroke-width="3" />
<rect x="300" y="170" width="34" height="34" rx="6" fill="#ffffff" stroke="#cfe8e3" stroke-width="3" />
</svg>
<div class="map__pin" aria-hidden="true">
<span class="map__dot"></span>
<span class="map__label">Paws & Whiskers</span>
</div>
</div>
<a class="map__link" href="https://maps.example.com/?q=148+Maple+Hollow+Rd" target="_blank" rel="noopener">Get directions →</a>
</section>
</aside>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Contact & Location
A warm, approachable contact panel for a neighbourhood veterinary clinic. On the left, an appointment-request form collects the owner’s name, pet name, phone, visit reason and an optional message, with inline validation that flags empty or malformed fields on blur and on submit before showing a success confirmation.
On the right, an info card lists the clinic address, a tappable phone number and the full weekly schedule. The current day is highlighted and an Open now / Closed badge is computed live from the visitor’s clock against the day’s hours. Below it, a stylized SVG map placeholder marks the clinic location without loading any third-party map.
Everything is vanilla HTML, CSS and JavaScript — no libraries, no network calls — so it drops straight into any page and is easy to rewire to a real backend.
Illustrative UI only — not for real veterinary use.