Freeframe

Documentation

Flow

v1.0.0Updated Oct 17, 2026A calm, airy yoga studio website with a live timetable and mat-by-mat class booking.

Overview#

Flow is a multi-page website template for an independent yoga studio, built for Salt & Sage Yoga, a fictional studio above the harbour in Falmouth, Cornwall. It is plain HTML5, Tailwind CSS v4 and one vanilla JavaScript file, with no framework and no build step needed to run it. The home page lists today's classes from the weekly timetable, the timetable can be filtered and shows what's on now and next, and visitors can reserve a specific mat and see it under My bookings. This guide covers version 1.0.0 (see template.json). Live demo: freeframe.dev/templates/flow.

Quick start#

  1. Unzip the download.

  2. Open index.html in a browser. Everything works from the file system, including booking a mat, the filters and the calculator.

  3. To preview over HTTP instead (closer to how it will run online), serve the folder with any static server:

    sh
    npx serve .

You only need Node.js 24 if you change src/input.css or add Tailwind classes that aren't already in the compiled CSS. See Building and deploying.

Pages#

File Purpose Notable sections and features
index.html Home Hero with a live "next class" card, today's classes (or tomorrow's after the last class), class styles, intro offer band, teachers, testimonials, workshops
timetable.html Timetable and booking My bookings, style/level/teacher filters, seven-day list with "On now" and "Up next", mat booking dialog
classes.html Class styles Seven styles with "good if" lists, length, room, teachers, next class and classes a week; level guide; health note
teachers.html Teachers Four profiles with training, styles, next class and a link to their filtered timetable
pricing.html Prices Intro offer, class passes, memberships, intro-offer calculator, short FAQ
workshops.html Workshops and retreats Event cards with places left, kind filter, booking enquiry form
guide.html Beginners' guide and FAQ Five steps, what to bring, health note, searchable FAQ accordion
contact.html Contact Illustrated map, address, today's opening times, travel notes, message form
404.html Not found Links back into the site

Project structure#

flow/
  index.html ... 404.html    the nine pages (header and footer are repeated in each)
  assets/css/style.css       compiled Tailwind CSS (generated: don't edit by hand)
  assets/js/main.js          all data and interactions, vanilla JavaScript
  assets/img/favicon.svg     the sun-over-the-sea mark
  assets/img/og-image.svg    social image source; og-image.png is the 1200x630 export
  assets/img/pose-*.svg      line drawings for the class styles (240x240)
  assets/img/teacher-*.svg   arch-shaped teacher portraits (240x300)
  assets/img/map.svg         illustrated map on the contact page (640x440)
  src/input.css              Tailwind source: theme tokens and components
  package.json               Tailwind CLI scripts for rebuilding the CSS
  README.md                  short introduction
  DOCS.md                    this guide
  CHANGELOG.md               version history
  LICENSE.txt                MIT license (in the download)

Customising#

Brand and colours#

All design tokens are in the @theme block at the top of src/input.css:

Token Use
--color-sand, --color-shell, --color-dune, --color-drift, --color-line Page background, cards, soft panels, deeper panels, borders
--color-sea-50 ... --color-sea-900 Brand colour: buttons and links (700), dark bands (900)
--color-clay-100 ... --color-clay-700 Warm accent: italic words, "On now", best-value bars
--color-sage-100 ... --color-sage-700 Level dots, ticks and leaves
--color-ink, --color-body, --color-mute Headings, body text, secondary text
--color-error Form errors

To change the brand colour, replace the sea scale with your own and rebuild the CSS:

css
@theme {
  --color-sea-700: #3b4f7a; /* buttons, links */
  --color-sea-900: #1f2a44; /* dark bands and footer */
}

Inline SVG artwork uses hex values directly (for example #2c524d in the logo and #1a3431 for line drawings), so search for the old hex codes in the HTML and assets/img/ if you change the palette a lot.

Typography#

Headings use EB Garamond (--font-serif, classes display, section-title, card-title) and body text uses Mulish (--font-sans). Both load from Google Fonts in the <head> of every page:

html
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..600;1,400..600&amp;family=Mulish:ital,wght@0,400..800;1,400..600&amp;display=swap">

To swap fonts, change that link on every page and the two --font-* tokens in src/input.css, then rebuild.

Content#

Page text lives in the HTML files. Everything the scripts compute from lives in constants at the top of assets/js/main.js:

Constant What it holds
STUDIO Name, timeZone (Europe/London), contact details, daysAhead (7), bookingCutoff (minutes before the start when booking closes, 0), demoFill (share of mats shown as taken, 0.45)
ROOMS loft (16 mats, 4 per row) and garden (10 mats, 5 per row)
TEACHERS morwenna, jago, anouk, kerensa: name, short name, role
STYLES vinyasa, hatha, slow, yin, restore, pregnancy, beginners
LEVELS open (All levels, 0 dots), gentle (1), steady (2), strong (3)
CLASSES The weekly timetable: 30 classes with id, day (0 = Sunday), start (24-hour UK time), mins, style, level, teacher, room
PRICES Drop-in price, intro offer (£35 for 3 weeks), concession (20%), and the five options the calculator compares
EVENTS Workshop and retreat ids, titles, kinds and dates: a rolling rule (for example the 1st Saturday of next month) or fixed UK date/end
TOPICS Contact form topics that ?topic= can preselect

To add a class, add a line to CLASSES with a unique id (bookings are saved against it):

js
{ id: 'thu-1230', day: 4, start: '12:30', mins: 45, style: 'slow', level: 'gentle', teacher: 'kerensa', room: 'garden' },

To add a style, teacher or level, add it to the matching constant, add an <option> with the same key to the filters in timetable.html, and add its section to classes.html or teachers.html with id="<key>" (the timetable links to classes.html#<style> and teachers.html#<teacher>).

Prices appear twice: in the cards in pricing.html and in PRICES for the calculator. Change both. Workshops also appear twice: the cards and form options in workshops.html (each card has data-event="<id>") and EVENTS.

Workshop dates: rolling or fixed

So the demo always has upcoming events, each event in EVENTS has a rolling rule, worked out from today's date in London: monthsAhead (1 = next month), weekday (0 = Sunday ... 6 = Saturday) and nth (1–4, or -1 for the last one in the month). days sets how long it lasts (default 1). For your real programme, replace the rule with fixed UK dates; an event with a date ignores its rule:

js
// Rolling: the 3rd Sunday of next month
inversions: { title: 'Arm balances & inversions, step by step', kind: 'workshop', rule: { monthsAhead: 1, weekday: 0, nth: 3 } },
// Fixed: a two-day event on 13–14 March 2027 (end is optional)
'new-year': { title: 'Beginners’ weekend', kind: 'course', date: '2027-03-13', end: '2027-03-14' },

The dates on the cards ([data-event-when="<id>"]), the short dates on the home page teasers ([data-event-short="<id>"]) and the enquiry form options are filled in from EVENTS. Without JavaScript the cards read "Dates on request". Keep the ids: ?event=<id> links use them.

Images and artwork#

There are no photos. All artwork is SVG drawn for this template:

  • pose-*.svg (240x240): a line figure over a coloured pebble, used for class styles.
  • teacher-*.svg (240x300): arch-shaped portraits. To use photos, keep a 4:5 ratio and the arch class on the wrapper so they get the same rounded top.
  • The home hero scene is inline SVG in index.html (460x560, arch frame).
  • map.svg is an illustration, not a real map. Replace it with a screenshot of your own map or an embedded map.
  • og-image.svg is the social image source. Export it to og-image.png (1200x630) after editing; social sites don't show SVG. favicon.svg is the browser tab icon.

The header and footer are repeated in every HTML file. To add a page, copy an existing page, then add a link to the <nav aria-label="Main"> list, the mobile menu (#mobile-menu) and, if you like, the footer, in every file. Mark the current page's link with aria-current="page". The desktop menu shows from 1280px (xl:); below that the menu button opens the mobile menu.

Features#

Today's classes and next class#

On the home page [data-today-list] lists today's classes with their state, and [data-next-class] shows the next class that hasn't started yet, with a button to reserve a mat. Times come from CLASSES and the current time in STUDIO.timeZone (Europe/London), whatever the visitor's own time zone. Once today's last class has finished the list switches to tomorrow ("Tomorrow at the studio"), so the page never looks empty. A class is "On now" between its start and end, and the first class after now is "Up next".

Timetable and filters#

timetable.html shows today plus the next six days (STUDIO.daysAhead), one section per day, built by renderTimetable() inside [data-timetable]. Days with no matching classes are skipped and a count is announced in [data-tt-count]. The three selects in [data-tt-filters] filter by style, level and teacher; their values are kept in the URL so filtered views can be shared:

timetable.html?style=yin
timetable.html?teacher=jago&level=strong

URL values are only used when they are own keys of STYLES, LEVELS or TEACHERS (Object.prototype.hasOwnProperty.call), so ?style=constructor is ignored. If nothing matches, [data-tt-empty] appears with a button to clear the filters.

Mat booking#

Every bookable class has a button with data-book="<class id>@<YYYY-MM-DD>". It opens the native <dialog> [data-book-dialog] with a room plan ([data-mat-grid]): one radio button per mat, laid out ROOMS[room].cols per row with the teacher at the front. Mats shown as taken are demo data from takenMats(), a stable hash of the class and date controlled by STUDIO.demoFill; at least two mats are always left free. Set demoFill to 0 when you connect real data.

Reserving saves the booking (see Saved data), updates the header count ([data-booking-count]) on every page, the My bookings list on the timetable ([data-my-bookings]) and the class row ("Mat 7 booked"). Opening a booked class lets you move to another mat or cancel. Classes that have started or finished can't be booked; STUDIO.bookingCutoff closes booking earlier if you set it above 0.

This is a front-end demo: bookings live only in the visitor's browser. To take real bookings, connect a booking system (Momence, Mindbody, bsport, TeamUp and others have widgets or APIs) and replace the confirm handler on [data-book-confirm] in main.js with a call to it, or link the "Reserve a mat" buttons to its booking pages.

Accessibility: the dialog is a modal <dialog> (focus is trapped, Esc closes it and focus returns to the button that opened it). Mats are a radio group inside a <fieldset> with a legend, taken mats are disabled and announced as "taken", and confirmations appear in a polite live region ([data-toast]).

Class styles and teachers#

On classes.html and teachers.html, [data-next-of="style:<key>"] and [data-next-of="teacher:<key>"] show the next class of that style or teacher, and [data-weekly-of="..."] shows how many classes a week they have, both computed from CLASSES.

Intro-offer calculator#

[data-price-calc] on pricing.html takes classes a week (1–6, a range input) and a concession checkbox. It shows what the intro offer costs per class over three weeks and how much it saves on drop-in prices, then works out a month's cost for every option in PRICES.options (52 weeks / 12 months): drop-in, 5-class pass, 10-class pass, Eight a month (plus extra per class over eight) and Unlimited. The cheapest is highlighted and bars compare the rest. The concession only applies to memberships. Results are announced through aria-live.

Workshops and retreats#

Each card in workshops.html has data-event="<id>". Dates come from EVENTS (rolling rules or fixed dates, see Workshop dates) and the cards are sorted by date. Events whose end date is before today are hidden, and their option is removed from the enquiry form. The "Everything / Workshops / Retreats" buttons in [data-kind-filter] use aria-pressed. An "Enquire" button preselects the event in the form, and workshops.html?event=scilly#enquire does the same from another page (only ids in EVENTS are accepted).

[data-faq-search] on guide.html filters the <details data-faq> items as you type, highlights the match in each question with <mark>, opens items whose match is only in the answer, hides empty groups and announces the count. guide.html?q=mat pre-fills the search. The accordion uses native <details>/<summary>, so it works with the keyboard and without JavaScript.

Small extras#

  • Header gets a hairline when you scroll; mobile menu with aria-expanded, Esc and click-outside to close.
  • Scroll reveal (.reveal), off with reduced motion; gentle drifting cards and tide lines in the hero.
  • contact.html shows today's first door-opening time and last class end in [data-today-hours].
  • contact.html?topic=teaching preselects a topic (own keys of TOPICS only).
  • A back-to-top button appears after one screen of scrolling.

Forms#

There are two forms, both with novalidate, data-validate-form, inline errors and a success state:

Form Page Validates
Booking enquiry ([data-enquiry-form]) workshops.html Event chosen, name, valid email, optional phone format, terms ticked
Message ([data-contact-form]) contact.html Name, valid email, topic chosen, message of at least 10 characters

Each field names its error element with data-error-id and is linked to it with aria-describedby. On submit, the first invalid field gets focus and a summary appears in a role="alert" paragraph. The success state says plainly that the demo doesn't send anything.

The demo forms don't send anything until you connect them. To use Formspree (or any form service that accepts a POST), replace the demo success block in the form[data-validate-form] submit handler in assets/js/main.js:

js
fetch('https://formspree.io/f/your-id', {
  method: 'POST',
  body: new FormData(form),
  headers: { Accept: 'application/json' }
}).then(function (res) {
  if (!res.ok) throw new Error('Send failed');
  form.hidden = true;
  done.hidden = false;
  done.focus();
}).catch(function () {
  summary.textContent = 'Sorry, that didn’t send. Please email hello@saltandsageyoga.co.uk.';
  summary.hidden = false;
});

Give each form an action with the same URL and method="post" if you also want it to work without JavaScript.

Saved data (localStorage)#

Key Shape Validated on read
flow.bookings [{ "c": "mon-0700", "d": "2026-10-19", "m": 7 }]: class id, UK date, mat number Must be an array; c must be an own key of the classes; d must be YYYY-MM-DD on the class's weekday; m a whole number within the room's mats. Duplicates and classes that have finished are dropped and the cleaned list is written back.

All access goes through one safeStorage object. When localStorage is blocked (sandboxed iframes, strict privacy settings) it falls back to an in-memory store, so booking still works for the rest of the page view. To reset, run localStorage.removeItem('flow.bookings') in the browser console or cancel each booking under My bookings.

SEO and sharing#

Every page has its own <title>, meta description, og:title, og:description, og:image and theme-color in its <head>. Before you launch:

  • Set og:image to an absolute URL, for example https://www.example.co.uk/assets/img/og-image.png.
  • Add <link rel="canonical"> with each page's full URL.
  • Add a sitemap.xml listing the pages and a robots.txt that points to it.
  • Consider LocalBusiness / ExerciseGym structured data (JSON-LD) with your address and opening hours.

Accessibility#

  • Skip link, landmarks (header, nav, main, footer), one <h1> per page and a logical heading order.
  • Visible focus rings (:focus-visible), WCAG AA colour contrast on sand, shell and dune backgrounds.
  • Native <dialog> for booking, a radio group for mats, aria-pressed filter buttons, <details> for accordions.
  • Live regions for timetable counts, FAQ results, the calculator and booking confirmations.
  • prefers-reduced-motion turns off reveals, drifting and the breathing dot.

When editing, keep one <h1> per page, give new inputs a <label>, and make sure every aria-describedby, aria-labelledby and aria-controls points at an id that exists.

Building and deploying#

The compiled CSS is committed, so you can upload the folder as it is. To change styles or add Tailwind classes that aren't used yet, install Node.js 24 and run:

sh
npm install
npm run build   # or: npm run watch

Classes used only inside main.js strings are picked up because Tailwind scans the JavaScript file too.

To deploy, upload the folder to any static host: Netlify Drop, Cloudflare Pages, GitHub Pages or your own server. There is no server code. Remove src/, package.json and DOCS.md from the upload if you don't want them public.

Troubleshooting#

  • Classes show at the wrong time. Times are UK time (STUDIO.timeZone). If your studio is elsewhere, change the time zone and the "UK time" wording in the HTML.
  • A booking disappeared. Bookings are removed once the class has finished, and they live in one browser only. In a private window or with storage blocked they last for the current page view.
  • A filter link doesn't filter. The value must match a key in STYLES, LEVELS or TEACHERS exactly (?style=restore, not ?style=restorative).
  • A new class doesn't show. Check its day (0 = Sunday) and that its style, level, teacher and room keys exist in the matching constants.
  • New Tailwind classes have no effect. Rebuild the CSS with npm run build.
  • A workshop vanished. Events with a fixed date are hidden once it has passed; update the date, switch back to a rolling rule, or remove the card.
  • The social preview shows no image. Use an absolute URL for og:image and the PNG, not the SVG.

License#

MIT. The code and all included artwork are free for personal and commercial use, including client work. The full license is in LICENSE.txt in the download. EB Garamond and Mulish are licensed under the SIL Open Font License. Salt & Sage Yoga, its teachers, students, reviews, events and prices are fictional.

Search documentation

Search the guides and every template's documentation