Freeframe

Documentation

Paws

v1.0.0Updated Oct 18, 2026A warm, friendly website for an independent vet clinic, with appointment requests and a vaccine planner.

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#

  1. Unzip the download.
  2. Open index.html in a browser. Everything works from the file system, with no server or build step.
  3. To preview over HTTP instead (closer to a real host), serve the folder, for example npx serve . or python3 -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:

css
@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:

html
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Calistoga&amp;family=Work+Sans:ital,wght@0,400..700;1,400..600&amp;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:

js
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:

js
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):

js
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) and team-*.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.html and planner.html also hold a hidden <svg> with <g data-art="..."> copies that main.js reuses for generated markup; keep them if you edit the art.
  • The social image is assets/img/og-image.png (1200 x 630), made from og-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.

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":

  1. 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".
  2. Visit type. Choices from SERVICES for that species, with length and starting price.
  3. 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 respect notice. BOOKING.demoBusyShare (0.35) marks some times as taken, using a stable hash so they don't change between visits; set it to 0 when you connect a real calendar.
  4. 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 Email

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:

html
<form action="https://formspree.io/f/your-id" method="POST" data-validate data-success-text="Thanks! We'll be in touch." novalidate>
js
/* 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), one h1 per page and a logical heading order.
  • Visible focus outlines, WCAG AA contrast, prefers-reduced-motion respected (no reveals, floats or pulses).
  • ARIA patterns: aria-pressed filter chips, day and time buttons; aria-expanded/aria-controls for the menu and articles; live regions for counts, the estimate, the planner and the toast; errors linked with aria-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:

sh
npm install
npm run build    # one-off, minified
npm run watch    # rebuild on save

Deploy 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 HOURS and CLINIC.timeZone in main.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 their shifts fall outside HOURS. Try "First available".
  • A new Tailwind class has no effect. Rebuild the CSS. Classes only used inside JavaScript strings must appear in main.js or the HTML for Tailwind to see them.
  • Saved pets disappear on reload. The browser is blocking storage (private mode or a sandboxed preview), so safeStorage keeps them in memory only. On a normal site they persist.
  • The planner shows the wrong stage. Stages are in weeks (from/to); PLANNER_UNITS converts months (52/12 weeks) and years (52 weeks).
  • Pet art is missing in the saved pets list. Keep the hidden <svg> with data-art groups near the end of book.html and planner.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.

Search documentation

Search the guides and every template's documentation