Overview#
Paws is a multi-page website template for an independent veterinary clinic. The demo is Sweetgum
Animal Clinic, a fictional practice at 22 Larkin Hollow Road in West Asheville, North Carolina, with
three vets who see dogs, cats, rabbits and small exotic pets. It is built with plain HTML, Tailwind CSS
v4 and one vanilla JavaScript file (assets/js/main.js), with no other dependencies. This is version
1.0.0 (see template.json and CHANGELOG.md). Live demo:
freeframe.dev/templates/paws.
The demo never goes stale: opening hours, holidays, appointment times and planner dates are all worked out from today's date in the clinic's time zone, so there are no fixed dates to update.
Quick start#
- Unzip the download.
- Open
index.htmlin a browser. Everything works from the file system, with no server or build step. - To preview over HTTP instead (closer to a real host), serve the folder, for example
npx serve .orpython3 -m http.server, and open the address it prints.
You only need Node.js (version 24 recommended) if you want to rebuild the CSS after adding Tailwind classes; see Building and deploying.
Pages#
| File | Purpose | Notable sections and features |
|---|---|---|
index.html |
Home | Open-now badge, next check-up opening, pet tiles, emergency signs and live advice, services, vaccine planner form, vets, reviews, map and hours |
services.html |
Services | Ten services filtered by pet (?pet=), prices from, "Request this" links that carry the visit type and pet |
book.html |
Request a visit | Four-step request: pet (with saved pets), visit type, vet and time, your details; live summary |
planner.html |
Vaccine planner | Species, age and lifestyle give "due around now", "coming up", "every year" and lifestyle lists; core vaccine table; FAQ |
pricing.html |
Pricing guide | Estimator with ranges by pet size (?pet=), wellness plans, payment notes |
team.html |
Our team | Vets with the species they see, their days and a live "in today" badge; technicians; Ziggy the clinic cat |
new-clients.html |
New clients | Three steps, packing checklist with a counter, records transfer form, FAQ accordion |
articles.html |
Pet care | Ten articles filtered by pet, topic and search; each expands in place; #article-id opens one |
contact.html |
Contact | Contact details, hours with holiday notice, after-hours advice, contact form (?topic=), map |
404.html |
Not found | Friendly message and links home |
Project structure#
paws/
index.html ... contact.html, 404.html the pages (header and footer repeated in each)
assets/
css/style.css compiled Tailwind CSS (generated by npm run build, committed)
js/main.js all data and interactions
img/
favicon.svg sweetgum leaf and paw logo mark
hero-pets.svg home hero: dog, cat and rabbit on a cushion
map.svg illustrated map (home and contact)
team-*.svg six illustrated portraits
og-image.svg social image source (1200 x 630)
og-image.png social image used by og:image
src/input.css Tailwind source: theme tokens and components
package.json npm scripts to rebuild the CSS
README.md short introduction
DOCS.md this guide
CHANGELOG.md version history
LICENSE.txt license (added to the download)Customizing#
Brand and colors#
Colors are Tailwind theme tokens in the @theme block of src/input.css:
| Token | Use |
|---|---|
--color-persimmon-* |
Brand color: buttons (600), hover (700), links and eyebrows (700) |
--color-marigold-* |
Cheerful highlight fills: active nav, open-now pill when closed, planner header |
--color-sky-*, --color-leaf-* |
Secondary fills; leaf is also the "open now" green |
--color-ink |
Headings, dark sections and footer (warm cocoa) |
--color-body, --color-muted |
Body and secondary text |
--color-oat, --color-paper, --color-line |
Page background, cards and borders |
To change the brand color, replace the persimmon scale and rebuild:
@theme {
--color-persimmon-600: #2f6fb0; /* buttons */
--color-persimmon-700: #255a90; /* hover and links */
--color-persimmon-800: #1d476f;
}Keep white text on persimmon-600 at 4.5:1 contrast or better. A few colors are written into SVG files
and inline SVG (the logo, the map pin, the pets); search for #c2441a to update them too.
Typography#
Headings use Calistoga and everything else Work Sans, loaded from Google Fonts in the <head> of
every page:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Calistoga&family=Work+Sans:ital,wght@0,400..700;1,400..600&display=swap">The stacks are --font-display and --font-sans in src/input.css. To swap a font, change the link on
every page and the token, then rebuild the CSS.
Content#
Text lives in the HTML pages. Data that drives the interactive parts lives at the top of
assets/js/main.js:
| Constant | What it holds |
|---|---|
CLINIC |
Name, timeZone ('America/New_York'), zoneName and phone |
HOURS |
Opening hours by weekday, 24-hour strings; 0 is Sunday, null is closed |
HOLIDAYS |
Closing rules, worked out for every year (see below) |
EXTRA_CLOSED |
One-off closed dates, 'YYYY-MM-DD' |
SPECIES |
The four pet types used by booking: dog, cat, rabbit, exotic |
VETS |
Each vet's name, the species they see and their appointment shifts by weekday |
SERVICES |
Visit types for booking: label, length, species, which vets offer it, price text, notice |
BOOKING |
Slot step, days shown, demo "taken" share, maximum saved pets |
PLANNER, PLANNER_UNITS |
Vaccine and wellness stages per species (dog, cat, rabbit, ferret) |
PRICE_PETS, PRICES |
Estimator pet sizes and [low, high] price ranges |
PETS_KEY |
The storage key for saved pets, 'paws-pets' |
Opening hours appear in three places: HOURS in main.js (the open-now badge, today's row and
bookable times), and the hours lists in the HTML (<dl data-hours>, rows with data-day="0" to "6") in
the footer of every page, on the home page and on the contact page. Change them together:
var HOURS = {
0: null, // Sunday: closed
1: ['07:30', '19:00'], // Monday
6: ['08:00', '13:00'] // Saturday
// ...
};Time zone. Every clock-based feature uses CLINIC.timeZone (America/New_York, US Eastern time), not
the visitor's device. Set it to your clinic's IANA time zone, for example 'America/Denver'.
Holidays. The clinic closes on New Year's Day, Memorial Day, Independence Day, Labor Day, Thanksgiving and Christmas Day. They are rules, not dates, so they stay right every year:
var HOLIDAYS = [
{ name: 'Independence Day', month: 7, day: 4 }, // a fixed date
{ name: 'Thanksgiving', month: 11, weekday: 4, nth: 4 }, // 4th Thursday of November
{ name: 'Memorial Day', month: 5, weekday: 1, nth: -1 } // last Monday of May
];Holidays close the open-now badge, block booking days and show a notice on contact.html when one is
within 14 days. Add a staff day with EXTRA_CLOSED = ['2027-02-12'], and update the sentence under the
hours on the contact page.
Vets. Add a vet to VETS and a card on team.html (copy an <li id="..."> block, keep the id equal
to the key so team.html#key and book.html?vet=key work):
okoro: {
name: 'Dr. Ada Okoro', short: 'Dr. Okoro', pets: ['dog', 'cat'],
shifts: { 1: ['08:00', '16:00'], 4: ['10:00', '18:00'] }
}Keep the "In the clinic" days on team.html in step with shifts.
Visit types. Each entry in SERVICES becomes a choice in step 2 when its pets include the chosen
species. vets: ['mbatha', 'castellanos'] limits who offers it; null means any vet who sees that species.
notice is the minimum minutes before a same-day start (60 for sick visits, 120 otherwise). The cards on
services.html are plain HTML; their "Request this" links use book.html?service=<key>.
Services page. Each card is an <li id="..." data-pets="dog cat rabbit exotic">. The filter shows
cards whose data-pets contains the chosen pet; keep the id values wellness, vaccines, dental,
surgery, exotics and senior if you keep the home page links to them.
Articles. Each article on articles.html is an <li id="slug" data-article data-pets="..." data-topic="..."> with a toggle button (aria-controls="slug-body") and a hidden body
(id="slug-body"). Topics are the options of the #a-topic select; add an option to add a topic.
Prices. PRICES has a [low, high] range per pet size, or null when a service isn't offered for
that pet (the row is hidden). The checkbox list on pricing.html (<li data-est-item="key">) must use
the same keys. Wellness plan prices and the "From $..." prices on services.html are plain HTML.
Images and artwork#
All artwork is SVG drawn for the template:
hero-pets.svg(600 x 500),map.svg(640 x 420) andteam-*.svg(square, 160 x 160). To use photos, keep similar aspect ratios: portraits are shown square on vet cards and 4:3 on technician cards.- Pet heads (dog, cat, rabbit, guinea pig and ferret) are inline SVG in the pages.
book.htmlandplanner.htmlalso hold a hidden<svg>with<g data-art="...">copies thatmain.jsreuses for generated markup; keep them if you edit the art. - The social image is
assets/img/og-image.png(1200 x 630), made fromog-image.svg. To regenerate it after editing the SVG, convert it with any SVG-to-PNG tool at 1200 x 630. - The favicon is
assets/img/favicon.svg.
Navigation, header and footer#
The header, mobile menu and footer are repeated in every page. To add or remove a page, edit the
<nav aria-label="Main"> list, the #mobile-menu list and, if needed, the footer links on every page.
Mark the current page with aria-current="page" on its link. The dark emergency bar above the header is
also on every page; keep the phone number in it up to date.
Features#
Open now, next opening and emergency advice#
renderHours() in main.js highlights today's row in every [data-hours] list, fills every
[data-open-status] pill ("Open now · until 7:00 pm" or "Opens Monday at 7:30 am") and rewrites
[data-emergency-advice] on the home page: when open it says to call the clinic, when closed it says to
call the nearest 24-hour emergency vet now. It refreshes every minute. The home page card
[data-next-slot] shows the first free wellness exam ("Mon, Oct 19 · 8:00 am") from the booking engine.
Appointment request#
book.html has four steps inside <form data-booking>, each a <fieldset data-step="1"> to "4":
- Your pet. Saved pets appear as choices with a remove button, plus "A different pet". A new pet needs
a name and species; age is years and months. "Remember this pet on this device" (checked by default)
saves it to
paws-pets, and the count ([data-pet-count]) reads "1 saved pet", "2 saved pets" or "No saved pets yet". - Visit type. Choices from
SERVICESfor that species, with length and starting price. - Vet and time. "First available" or a vet who offers the visit and sees the species. The day picker
shows 7 days at a time for the next 21 days (
BOOKING.daysPerPage,BOOKING.pages); days without times are disabled. Times run every 30 minutes inside the vet's shift and the opening hours, skip holidays and respectnotice.BOOKING.demoBusyShare(0.35) marks some times as taken, using a stable hash so they don't change between visits; set it to0when you connect a real calendar. - Your details. Name, phone (10+ digits), email, first visit or not, notes (500 characters, with a counter) and a consent box.
The summary ([data-summary]) updates as you go. The success panel says "Request ready" with the details
and states that nothing was sent. URL parameters preselect choices: book.html?pet=cat&service=dental&vet=mbatha.
Each is ignored unless it is a key of SPECIES, SERVICES or VETS (own-key check).
Accessibility: each step's heading takes focus when shown, errors are announced (role="alert") and linked
with aria-describedby, day and time buttons use aria-pressed with full labels ("Monday, October 19, 12
times free"), and the progress list marks the current step with aria-current="step".
Vaccine and wellness planner#
planner.html reads the species (dog, cat, rabbit or ferret), an age from 1 to 30 in weeks, months or
years, and lifestyle boxes (daycare or boarding and woods for dogs, outdoors for cats, outdoor hutch for
rabbits). The age in weeks picks a stage from PLANNER[species].stages (each { from, to, name, items },
ages in weeks). It shows:
- Usually due around now: the current stage's items.
- Coming up: the next stage, dated from today ("in about 2 weeks" or "around November 2026"), so the dates roll forward on their own.
- Every year after that:
yearly. - Because of their lifestyle:
extras[box name]for ticked boxes.
Saved dog, cat, rabbit and ferret pets from the booking form appear as quick-fill chips. Changes update
the URL (planner.html?pet=dog&age=10&unit=weeks), and the home page form links to it the same way. The
guidance is general, and the page says so; edit PLANNER to match your clinic's protocols.
Services filter#
Chips in [data-pet-filter] filter the cards and set ?pet= (dog, cat, rabbit, exotic). The
count reads "Showing 9 services for cats". "Request this" links gain &pet= so the booking form starts
with that species. Buttons use aria-pressed; the count is a live region.
Pricing estimator#
<form data-estimator> sums the ticked services for the chosen pet size: "$124 – $170" for a medium dog
with a wellness exam and core vaccines. Items that don't apply to a pet are hidden and unticked, with a
note. ?pet= preselects a size (dog-s, dog-m, dog-l, cat, rabbit, exotic; dog means medium).
The box is labeled "Estimate only" and says it is not a quote.
Articles#
[data-article-filter] combines a pet chip, a topic select and a search box; the count reads "Showing 1
article" or "Showing 10 articles", with an empty state and "Clear filters". ?pet= and ?topic= are kept
in the URL. "Read the article" buttons toggle the body (aria-expanded, aria-controls). Opening
articles.html#rabbit-teeth clears filters if needed, opens that article and scrolls to it.
Team status#
Each vet card has [data-vet-status="key"], which shows "In today until 3:30 pm", "In today from 10:00 am"
or "In the clinic Tuesday", from VETS[key].shifts, the opening hours and holidays.
New-client checklist and FAQ#
The packing list on new-clients.html counts ticked items ("3 of 6 packed"). It is not saved. The FAQs
are native <details> elements, so they work with the keyboard and without JavaScript.
Mobile menu, reveals, back to top and toast#
The menu button toggles #mobile-menu (aria-expanded, Esc closes and returns focus). Elements with
.reveal fade in on scroll, and everything above is revealed too; reduced motion shows them at once. A
back-to-top button appears after one screen. toast() shows short messages such as "Waffles is saved on
this device" in [data-toast], a polite live region.
Forms#
Nothing is sent anywhere until you connect the forms. The forms are:
| Form | Page | Validates |
|---|---|---|
Appointment request (data-booking) |
book.html |
Pet, visit, time, name, phone, email, consent |
Records transfer (data-validate) |
new-clients.html |
Name, email, pets, previous clinic, consent |
Contact (data-validate data-contact-form) |
contact.html |
Name, email, message (10+ characters); ?topic= preselects the topic |
Newsletter (data-validate) |
Footer of every page |
Forms with data-validate show inline errors after the field (linked with aria-describedby) and, when
valid, show the data-success-text message in [data-success] and reset.
To connect a form to a service such as Formspree, add action and method to the form and send it with
fetch where the code says "Connect here" in assets/js/main.js:
<form action="https://formspree.io/f/your-id" method="POST" data-validate data-success-text="Thanks! We'll be in touch." novalidate>/* Connect here: */
fetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } })
.then(function (res) { if (!res.ok) throw new Error(); ok.textContent = form.getAttribute('data-success-text'); ok.hidden = false; form.reset(); })
.catch(function () { ok.textContent = 'Sorry, that didn’t send. Please call us.'; ok.hidden = false; });For the appointment request, do the same in the submit handler of bookingForm (the "Connect here"
comment), or post to your practice management software's booking API. Then remove the "This is a demo
template" note in the success panel on book.html.
Saved data (localStorage)#
| Key | Shape | Used by |
|---|---|---|
paws-pets |
Array of up to 6 { id, name, species, kind, born } |
book.html (saved pets), planner.html (quick-fill chips) |
Example: [{"id":"p1a2b3c","name":"Waffles","species":"dog","kind":"","born":"2024-10"}]. born is the
approximate birth month (YYYY-MM) worked out from the age entered, so ages stay current.
On read, loadPets() drops anything invalid: non-arrays, items without a valid id (p plus up to 16
letters or digits), empty names, a species that isn't an own key of SPECIES, a malformed or future
born, and duplicates. Names and kinds are trimmed to 30 characters. The booking page writes the cleaned
list back.
All access goes through safeStorage: localStorage when available, or an in-memory stand-in when it is
blocked (sandboxed iframes, strict privacy settings), so saving a pet still works for the page view. To
reset, remove a pet with its × button or delete paws-pets in the browser's developer tools.
SEO and sharing#
Every page has its own <title>, meta description, Open Graph tags and theme-color in the <head>.
og:image points at assets/img/og-image.png; most social sites need an absolute URL, so change it to
your domain, for example https://www.example-vet.com/assets/img/og-image.png. Add a sitemap.xml and
robots.txt on your host if you want, and consider VeterinaryCare structured data (JSON-LD) with your
address, phone and opening hours.
Accessibility#
- Skip link, landmarks (
header,nav,main,footer), oneh1per page and a logical heading order. - Visible focus outlines, WCAG AA contrast,
prefers-reduced-motionrespected (no reveals, floats or pulses). - ARIA patterns:
aria-pressedfilter chips, day and time buttons;aria-expanded/aria-controlsfor the menu and articles; live regions for counts, the estimate, the planner and the toast; errors linked witharia-describedby. - Decorative SVGs are
aria-hidden; the map has descriptive alt text.
When editing, keep one h1 per page, give every new input a <label>, and make sure every
aria-controls, aria-labelledby and aria-describedby points at an id that exists.
Building and deploying#
The compiled CSS is committed, so you can upload the folder as it is. To rebuild after editing
src/input.css or adding Tailwind classes to the HTML or main.js:
npm install
npm run build # one-off, minified
npm run watch # rebuild on saveDeploy by uploading the whole folder to any static host: Netlify Drop, GitHub Pages, Cloudflare Pages,
Vercel or ordinary web hosting. 404.html is used automatically by most of these hosts.
Troubleshooting#
- The open-now badge is wrong. Check
HOURSandCLINIC.timeZoneinmain.js. Times are 24-hour strings in the clinic's own time zone, not the visitor's. - No times appear in the booking form. The chosen vet may not see that species or offer that visit
(
VETS[...].pets,SERVICES[...].vets), or theirshiftsfall outsideHOURS. Try "First available". - A new Tailwind class has no effect. Rebuild the CSS. Classes only used inside JavaScript strings
must appear in
main.jsor the HTML for Tailwind to see them. - Saved pets disappear on reload. The browser is blocking storage (private mode or a sandboxed
preview), so
safeStoragekeeps them in memory only. On a normal site they persist. - The planner shows the wrong stage. Stages are in weeks (
from/to);PLANNER_UNITSconverts months (52/12 weeks) and years (52 weeks). - Pet art is missing in the saved pets list. Keep the hidden
<svg>withdata-artgroups near the end ofbook.htmlandplanner.html.
License#
MIT. The code and all included artwork are free for personal and commercial use; see LICENSE.txt in
the download.
The fonts (Calistoga and Work Sans) are from Google Fonts under the SIL Open Font License.
Sweetgum Animal Clinic, its staff, pets, reviews and address are fictional placeholders; replace them with your own content. The health information is general and should be reviewed by your own vets.