Freeframe

Documentation

Kinetic

v1.0.0Updated Oct 15, 2026A bright, energetic personal trainer website with a goal picker, a week-view booking flow with health questions, a pricing calculator and free training tools.

Overview#

Kinetic is a website for an independent personal trainer or a small personal training studio, built as a WordPress block theme. The fictional brand is Fenline Performance, a two-coach studio in Romsey, Cambridge (UK), run by Ffion Beckett and Yusuf Dacre. The theme has a home page with a goal picker that recommends a programme, the next free consultation, client progress charts, how it works, testimonials and a pricing summary with the Kickstart offer; a programmes page filterable by goal with a comparison table; a pricing page with packs, memberships, a cost calculator, an FAQ and the small print; a results page with six client stories and charts, filterable by goal; a training tools page with a plan builder, a one-rep max estimator and a running pace calculator; an about page with the coaches; a contact page with a form, map and live studio hours; a four-step booking flow with a week view and health questions; and a 404 page. It is version 1.0.0 (see template.json and style.css), works with WordPress 7.1 and PHP 7.4 or newer, and is the WordPress version of the HTML template of the same name. See it live at freeframe.dev/templates/kinetic.

Headings, copy, cards, lists, buttons and images are core WordPress blocks arranged in block patterns, so you edit them in the block editor without touching code. The interactive parts (the goal picker's choices, the filter chips, the charts, the calculators, the booking flow, the plan builder, the studio hours and the forms) work as in the HTML version: they are small Custom HTML blocks driven by assets/js/main.js. Colours and fonts are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site follows.

The copy uses British English, pounds sterling, 24-hour times and UK time (Europe/London). The content is general information for a fictional studio, not medical advice. Review all health copy, especially the health questions in the booking flow, with a qualified professional before publishing your own site.

Quick start#

Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.

  1. Download kinetic.zip from freeframe.dev/templates/kinetic (the WordPress tab).
  2. In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Kinetic.
  3. Go to Appearance > Kinetic setup and click Create the demo pages. This creates the Home, Programmes, Pricing, Results, Training tools, About, Contact and Book online pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu. Pages that already exist with the same slug are left alone, so it is safe to click it again.
  4. Visit your site. Edit any page in Pages, and the header, footer and templates in Appearance > Editor.

You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.

To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:

sh
pnpm install
pnpm dev     # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400

pnpm dev signs you in as admin (password password) and creates the demo pages. The site is rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the WordPress admin are not kept.

Pages#

Each page is a page pattern (in Patterns > Kinetic pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Kinetic.

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (next free consult, client progress card), ticker, goal-picker (five goals and their panels), programmes-preview, results-preview (three charts), how-it-works, testimonials, pricing-preview (plans, Kickstart, health note)
Programmes /programmes/ page-programmes.php programmes-hero, programmes-list (seven programmes with goal filter, ?goal=), programmes-compare (table), programmes-basics
Pricing /pricing/ page-pricing.php pricing-hero, kickstart (#kickstart), packs, memberships (#memberships), calculator (#calculator), pricing-compare, pricing-faq (#faq), terms (#terms)
Results /results/ page-results.php results-hero, stories (six stories with charts and goal filter, ?goal=), reviews
Training tools /tools/ page-tools.php tools-hero, plan-builder (#plan), calculators (#one-rep-max, #pace), tools-cta
About /about/ page-about.php about-hero, story, coaches (#ffion, #yusuf), approach, milestones, studio
Contact /contact/ page-contact.php contact-hero, contact-form (form, map, studio hours, #social), privacy (#privacy)
Book online /book/ page-book.php book-hero, booking (the four steps, the confirmation and the summary)
404 any missing URL templates/404.html 404

Posts use templates/single.html; the blog, archives and search results use templates/index.html, templates/archive.html and templates/search.html. For a plain text page (a privacy policy, for example) choose the Page with title template.

Project structure#

freeframe-kinetic/
  style.css                 theme header (name, version, requirements, license); no styles
  theme.json                design tokens: palette, fonts, spacing, layout widths, button
  functions.php             loads the files in inc/
  inc/setup.php             stylesheet and script (with its strings and the portraits' URLs),
                            editor styles, the `js` class script, theme colour and favicon, pattern
                            categories
  inc/icons.php             registers assets/icons/*.svg for the Icon block
  inc/icon-labels.php       the icons' names shown in the editor
  inc/block-styles.php      button block styles
  inc/site-setup.php        Appearance > Kinetic setup: creates the demo pages and the menu
  templates/                page.html (pages), page-with-title.html, single.html, index.html,
                            archive.html, search.html, 404.html
  parts/                    header.html and footer.html (they show the header and footer patterns)
  patterns/                 every section and page (PHP files with block markup)
  assets/css/theme.css      the compiled stylesheet (do not edit: built from src/)
  assets/css/editor.css     the same for the block editor (generated by src/editor-css.mjs)
  assets/js/main.js         studio data, hours, booking, calculators, plan builder, charts, forms...
  assets/icons/             SVG icons for the Icon block
  assets/img/               athlete pictograms, coach portraits, studio, map, logo, favicon, social image
  assets/fonts/             Sofia Sans and Sofia Sans Extra Condensed, with their licenses
  src/theme.css             the Tailwind CSS source of assets/css/theme.css
  src/icons.css             icons drawn before or after text with CSS (generated)
  src/editor-css.mjs        writes assets/css/editor.css after each build
  languages/                freeframe-kinetic.pot, the template for translations
  blueprint.json            WordPress Playground setup used by `pnpm dev`
  screenshot.png            the picture shown in Appearance > Themes
  package.json              the CSS build and the local WordPress (Node.js tooling)
  readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txt

Customising#

Brand and colours#

The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles > Colors > Palette:

Colour Default Used for
Cobalt 50 to 950 #eef0ff to #10163f the brand blue: buttons and links (600), dark bands and the footer (900, 950)
Flare 50 to 800 #fff3ee to #8f2a12 the energy colour: highlights, button shadows and stripes; 700 and 800 for text on light backgrounds
Sun 100 to 700 #fff6c9 to #7a5c00 small highlights: today's row in the studio hours, badges
Mint 50 to 800 #ecfbf4 to #0b5a40 "included" ticks, the open-now badge, positive outcomes
Ink, Body, Muted #0f1330, #383d5a, #575c7c headings, body text and secondary text
Line, Paper, Mist #e4e1d8, #f6f4ef, #eceae3 borders, the page background and quiet fills
White, Black for your own blocks

The theme's stylesheet reads these colours from the palette (bg-cobalt-600, text-flare-700...), so changing Cobalt 600 in Styles recolours every primary button at once. To move to another brand colour, change the whole Cobalt scale together and keep the 600 shade dark enough for white button text (4.5:1). The pictures (assets/img/*.svg), the SVGs in Custom HTML blocks (the hero athlete, the page header panels, the 404 lane) and the chart code (renderChart() in assets/js/main.js) carry their own hex colours: search for #2b3be0 (cobalt), #ff5a33 (flare) and #0f1330 (ink) to change them too. The browser UI colour (theme-color) is set in freeframe_kinetic_head_meta() in inc/setup.php.

Component classes from the HTML template work on blocks too: add them in Block settings > Advanced > Additional CSS class(es). card gives a group the white card look, card-hover lifts it on hover, tag with tag-cobalt, tag-flare, tag-sun, tag-mint or tag-ink makes a small label, kicker is the label with a slanted flare bar above a heading, section-title, display and card-title are the italic condensed heading styles, lead the intro paragraph, hl (on a span inside a heading) the slanted flare highlighter, tick-list on a List block the green ticks, note the white note with a flare edge, lanes and lanes-light the running-track stripes, slant-top and slant-both the diagonal section edges (with [--slant:4vw] to change the angle), and text-link the underlined bold link.

Typography#

Two fonts are bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google: Sofia Sans Extra Condensed (variable, 600 to 900, with italics; preset display) for the big italic headlines, card titles and buttons, and Sofia Sans (variable, 400 to 900, with italics; preset sans) for text. To use other fonts, install them in Styles > Typography > Fonts (the Font Library) and select them, or replace the fontFace entries in theme.json. The utility classes read the presets (--wp--preset--font-family--display and --sans), so keep those slugs. Condensed display faces suit the layout best; wider faces need smaller heading sizes.

Content#

Edit pages in Pages: click any text to change it, select a button to change its link. Sections are groups; open List View (the stacked lines icon at the top left) to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Kinetic.

What the script drives, and pictures drawn in SVG, are Custom HTML blocks: edit those in the block's HTML view, and keep their ids and data- attributes.

Pattern Custom HTML What it is
home-hero the athlete picture, the track lanes, the "Next free consult" card, the small chart pictures and the card the script fills in
ticker the scrolling goal band decorative
goal-picker the five goal cards (radio buttons) see Goal picker; the five panels are blocks
results-preview, stories the charts (figure data-chart) see Progress charts
programmes-list, stories the filter chips, the live count and the empty message see Programme and story filters
programmes-compare, memberships, pricing-compare the tables tables with a scrolling frame on phones
calculator the calculator see Pricing calculator
plan-builder, calculators the tools see Plan builder and One-rep max and pace calculators
booking the four steps, the confirmation and the summary see Booking flow
contact-form, footer the form and its thank-you message; the open-now badge and studio hours see Forms and Studio hours and open-now status
the page headers, 404, about-hero the cobalt panel, illustrated "UI" cards and portraits decorative pictures
header, footer the menu button; the toast driven by the script

Some blocks carry classes (Block settings > Advanced > Additional CSS class(es)) that the script reads. Blocks can't store data- attributes or hidden, so the script turns classes starting with ffd- into attributes when the page loads, and ff-hidden into hidden: ffd-goals--strength_sport on a programme card becomes data-goals="strength sport", and ffd-goal-panel--fatloss becomes data-goal-panel="fatloss". ff-live becomes aria-live="polite".

  • reveal fades a block in as it scrolls into view (reveal-x slides it in from the left); a class such as [--d:80ms] delays it.
  • link-card on a group makes the whole group clickable: the first link inside it stretches over the group (the programme cards on the home page, the logo).

Studio data that drives the interactive features lives in constants at the top of the second part of assets/js/main.js, as in the HTML version:

Constant What it holds
STUDIO name, time zone (Europe/London), time zone label, phone, email and address (used in .ics files)
HOURS opening hours by weekday (0 = Sunday, null = closed), 24-hour HH:MM strings
CLOSED_DATES bank holidays and closures as YYYY-MM-DD
COACHES each coach's name, short name, role, portrait and shifts (1:1 hours by weekday)
GROUP_CLASSES, GROUP_SIZE small-group classes (weekday, start time, coach, name) and the class size
SESSIONS bookable session types: label, minutes, price, price label, coaches, notice and whether health answers trigger a call-back
BOOKING slot spacing, weeks shown and the demo "busy" share
GOALS the five goal ids and labels used by the goal picker, filters, booking and plan builder
PRICING calculator prices: pay per session, packs, membership rates
RESULTS chart data for client stories
FOCUS, PLAN_DAYS, SPLITS, SCHEMES, EQUIPMENT plan builder rules and exercises

Visible strings in those constants are wrapped in __(), which looks them up in the translations passed from PHP (see Translations). The studio hours also appear as text in the Custom HTML hours lists (<dl data-hours>) in the footer and on the contact page: when you change HOURS, update those lists too. Prices appear in the programme cards, the pricing tables, the home page and SESSIONS and PRICING: keep them in step.

Adding a coach

Add an entry to COACHES and list its id in the coaches of each session they offer (in a child theme, see Child themes):

js
var COACHES = {
  // ...
  maya: {
    name: __('Maya Lin'), short: __('Maya'), role: __('Coach'), img: asset('assets/img/coach-maya.svg'),
    shifts: { 2: [['16:00', '20:30']], 4: [['16:00', '20:30']], 6: [['08:00', '12:00']] }
  }
};

asset() looks the path up in the portrait URLs that freeframe_kinetic_script_assets() in inc/setup.php passes to the script: add the new file's name to its list (or give img a full URL, for example of an image in the Media Library). Then add a radio card to step 2 in the booking pattern's Custom HTML (copy an existing coach card, including data-coach-next="maya"), and duplicate a coach profile on the About page in List View. Link straight to a coach with /book/?coach=maya.

Adding a programme or a client story

On the Programmes page, duplicate a programme card (an article group) in List View and change its text, picture and links. Its class ffd-goals--… lists the goal ids it appears under, joined with underscores (ffd-goals--strength_sport); the chip counts are worked out by the script. On the Results page, a story works the same way. For a new chart, add an entry to RESULTS and a figure in a Custom HTML block:

html
<figure class="chart" data-chart="plank" data-reveal>
  <div data-chart-svg></div>
</figure>
js
plank: { label: __('Plank hold'), unit: 'reps', x: [__('Wk 1'), __('Wk 4'), __('Wk 8')], y: [20, 45, 70], better: 'up' }

unit can be kg, cm, reps, steps or time (values in seconds, shown as m:ss). With better: 'down' the axis is flipped so improvement still rises.

Adding a session type

Add it to SESSIONS and a matching radio card (name="type", same value) to step 1 in the booking pattern's Custom HTML:

js
assessment: { label: __('Fitness assessment'), minutes: 60, price: 55, priceLabel: __('£55'), coaches: ['yusuf'], notice: 1440, screen: true }

Images and artwork#

All artwork is SVG in assets/img/, used by Image blocks: select one and Replace it.

  • pose-strength.svg, pose-fatloss.svg, pose-mobility.svg, pose-sport.svg and pose-postnatal.svg are 120 × 120 pictograms. On the programme cards they are turned white with the classes [&_img]:brightness-0 [&_img]:invert, so keep them single-colour plus flare accents.
  • coach-ffion.svg and coach-yusuf.svg are square portraits on the About page and in the booking confirmation (COACHES[...].img). To use photos, keep a square crop of at least 480 × 480 pixels, upload them to the Media Library, replace the Image blocks and change the img paths in the script (a full URL works) so the booking flow shows the same picture. Keep the alt text describing the person.
  • studio.svg (16:10) is on the About page and map.svg (10:7) on the Contact page. You can swap the map for an embedded map, but that adds external requests and cookies.
  • The home hero athlete, the page header panels, the ticker and the 404 art are inline SVG in Custom HTML blocks; replace any of them with an Image block if you prefer a picture.
  • The logo is assets/img/logo.svg in an Image block next to the Fenline name in the header and footer patterns. The social image is assets/img/og-image.png (1200 × 630, source og-image.svg); the favicon assets/img/favicon.svg is used until you set a Site Icon in Settings > General.

Icons are Icon blocks: select one and pick another from the Kinetic collection or WordPress's own.

The header and footer are template parts (parts/header.html, parts/footer.html) showing the header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer. The header pattern also holds the dark announcement bar above the header (the Kickstart offer and the phone number).

The header has two Navigation blocks showing the same menu: the desktop one and the one in the mobile menu. Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main menu": Home, Programmes, Pricing, Results, Training tools, About, Contact, Book online). Its first link (Home) and last link (Book online) are hidden in the desktop header, which has the logo and its own "Book a free consult" button; keep them first and last, or remove the .site-nav … :first-child and :last-child rules in src/theme.css. The desktop menu shows from 1280 px wide; below that the menu button opens the mobile menu. A Navigation block that doesn't point at a menu shows the most recently created one; to pin a menu, select the Navigation block and choose it in the block settings, for both blocks. WordPress marks the current page in the menu; the script marks the "Book a free consult" button on the booking page.

The footer's Train and Studio menus are List blocks: click a link to change it, press Enter to add one. The address, phone number and email also appear on the contact page and in STUDIO; search for them when you change them. The Instagram and YouTube buttons are Button blocks with the Icon only style: change their links in the block toolbar.

Features#

Goal picker#

On the home page, five radio cards (input[name="goal"], in the goal-picker pattern's Custom HTML) inside the group with the class ffd-goal-picker show one of five panels: groups with ffd-goal-panel--strength, ffd-goal-panel--fatloss and so on. Edit the panels as blocks. The choice is saved under the kinetic-goal key and preselected next time; /?goal=sport also selects a goal. The panels are an aria-live="polite" region (the class ff-live) and the cards are a native radio group (arrow keys move between goals).

Programme and story filters#

The Programmes and Results pages use the same filter. A section with ffd-filter holds chip buttons ([data-filter-value], aria-pressed, in Custom HTML) and items with ffd-goals--…. With ffd-filter-url the choice is written to ?goal= with history.replaceState, so /programmes/?goal=postnatal opens filtered. Unknown values fall back to "All". A status line ([data-filter-status], role="status") announces how many items are shown, and choosing a goal also updates kinetic-goal.

Progress charts#

renderChart() in main.js draws each <figure data-chart="id"> from RESULTS as an inline SVG line chart with grid lines, axis labels, an area fill and a highlighted final value. Each SVG has role="img" and an aria-label summary such as "5 km time: 31:12 at Wk 1, 26:28 at Wk 12 (lower is better)." The line draws in when scrolled into view; with reduced motion it appears immediately.

Next free consult#

The "Next free consult" card on the home page ([data-next-consult]) shows the first bookable consultation from the booking engine, for example "Today 13:30 with Yusuf". It stays hidden if JavaScript is off.

Booking flow#

The Book online page holds a form inside [data-booking] with four steps and a confirmation; the code is initBooking() in assets/js/main.js:

  1. Session: radio cards name="type" matching the keys of SESSIONS.
  2. Coach: name="coach" with any (no preference), ffion or yusuf. Coaches who don't offer the chosen session are disabled, and each card shows its next opening ([data-coach-next]).
  3. Time: a week view (Monday to Sunday) with previous and next week buttons, for this week and the next BOOKING.weeksAhead - 1 weeks. It opens on the week of the first available time.
  4. Your details: name, email, mobile, optional goal and notes, seven yes/no health questions and a consent box.
  5. Confirmation: a summary, the coach's portrait and an Add to calendar (.ics) button.

How times are built, in slotsFor():

  • A 1:1 session can start every BOOKING.slotStep minutes inside the coach's shifts and the studio HOURS, and must finish by the end of the shift.
  • It can't overlap a small-group class the coach is leading (GROUP_CLASSES, 60 minutes).
  • It must start at least notice minutes from now (2 hours for consults, 12 hours for training), in UK time.
  • CLOSED_DATES and closed weekdays show "Studio closed" / "Closed".
  • group sessions use the fixed GROUP_CLASSES times and show the spaces left.
  • With "No preference", each time goes to the first coach in the session's coaches list who is free.
  • BOOKING.demoBusyShare hides a stable, pseudo-random share of times so the demo looks realistic. Set it to 0 when you connect real availability.

URL parameters preselect choices: /book/?type=pt60&coach=yusuf&goal=sport. type must be a SESSIONS key, coach a COACHES key that offers that session, and goal a GOALS key (it fills the goal field; otherwise the saved kinetic-goal is used).

All dates and times are calculated and shown in STUDIO.timeZone (Europe/London), whatever the visitor's own time zone. The .ics file stores the start and end in UTC, so calendars convert it correctly.

Accessibility: each step heading receives focus when the step opens, the progress bar marks the current step with aria-current="step", step errors use role="alert", each day in the week view is a labelled group and every time is a radio with a full label (for example "Tuesday, 20 October 2026, 07:30, Ffion").

Health questions#

Step 4 asks seven PAR-Q style yes/no questions (name="parq1" to parq7). They are required. As soon as they are answered, [data-health-outcome] shows one of three messages:

Outcome When Result
clear All answers are no The booking confirms as normal
talk Any yes, for a session with screen: true The button changes to "Send call-back request" and the confirmation explains that it is a request, not a booking, with no .ics file
consult Any yes, for the free consultation (screen: false) The booking goes ahead, with a note that the consult is the right place to talk it through

The wording is deliberately cautious: it asks people to talk to the coach first and to check with their doctor. Keep it that way, and have a professional review the questions for your country before you publish. Health answers are special category data under UK GDPR: if you connect this form, store them securely and only as long as you need.

Pricing calculator#

The calculator pattern ([data-calculator]) has a sessions-a-week stepper (1 to 4) and a 45/60-minute toggle. For the chosen combination it compares, per month (PRICING.weeksPerMonth = 52 / 12):

  • Pay per session: PRICING.payg[length] × sessions a month.
  • Session pack: the largest pack in PRICING.packs[length] you'd use within PRICING.packWeeks, priced per session.
  • Monthly membership: PRICING.member[length][perWeek - 1] × sessions a week × 52 / 12, rounded to the pound.

The cheapest of pack and membership is marked "Best value", and the saving against pay per session is shown per month and per year. The results list is aria-live="polite". Change prices in PRICING and in the tables on the page.

Plan builder#

[data-plan-builder] on the Training tools page builds an illustrative week from four radio groups: days a week (2 to 5), goal, equipment (gym, dumbbells, bodyweight) and session length (30, 45 or 60 minutes, showing 3, 4 or 5 exercises). Training days come from PLAN_DAYS, the session focus for each day from SPLITS[goal][days], exercises from FOCUS (each exercise is [gym, dumbbells, bodyweight]) and the sets and reps from SCHEMES or the focus's own scheme. Rest days are filled in automatically. The postnatal goal shows an extra safety note.

The inputs are saved under kinetic-plan and restored on the next visit. Print calls window.print() (the print styles hide the header and footer); Reset clears the saved plan.

One-rep max and pace calculators#

[data-orm] estimates a one-rep max with the Epley formula (weight × (1 + reps / 30), or the weight itself for one rep), rounded to 2.5 kg or 5 lb, and lists 60 to 95% training weights from ORM_TABLE. Reps are limited to 1 to 12.

[data-pace] turns a distance (5 km, 10 km, half marathon, marathon or a custom distance up to 250 km) and a finish time into pace per km, pace per mile and speed, plus finish times for the standard distances at the same pace.

Studio hours and open-now status#

Every [data-hours] list highlights today's row, and every [data-open-status] badge shows "Open now · until 20:30" or "Closed · opens tomorrow 06:00", based on HOURS and CLOSED_DATES in UK time (not the visitor's clock). They refresh every minute.

Block styles and small extras#

In the block settings' Styles panel:

Block Styles
Button Fill (default, the cobalt button with a flare shadow), Outline, Flare, White, Ghost, Icon only

The classes is-large and is-small on a Button block make it the hero or compact size. Icons on buttons and links are classes too (icon-before-calendar, icon-after-arrow-right, with icon-size-20); see src/icons.css for the available icons.

  • FAQ: the Pricing FAQ is a set of Details blocks (native <details>, class faq): duplicate one to add a question. The "+" square that turns into "×" is drawn by CSS (.faq > summary::after in src/theme.css).
  • Mobile menu: [data-menu-button] toggles #mobile-menu, updates aria-expanded and its label, and closes on Esc (returning focus to the button), outside clicks or link clicks.
  • Header: gets a shadow after scrolling.
  • Scroll reveals: reveal and reveal-x blocks fade in once; revealing one also reveals everything above it. Turned off with reduced motion, and nothing is hidden without JavaScript.
  • Ticker: the scrolling goal band on the home page is decorative (aria-hidden) and stops with reduced motion.
  • Back to top: created by the last part of main.js; delete that part and the .back-to-top styles in src/theme.css (then pnpm build) to remove it.

Forms#

Every form is a demo: it validates in the browser and shows a success state, but nothing is sent, booked or stored until you connect it. The confirmation screens say so.

Validation is driven by attributes in the forms' Custom HTML, handled by validateFields() and fieldMessage() in assets/js/main.js:

Attribute Rule
required must be filled, ticked or chosen (data-required-msg for a custom message)
type="email" must look like an email address
data-phone at least 10 digits
minlength minimum length
data-label the field's name in messages
data-validate opts the field into validation

Errors appear under the field (or under the group for radio buttons), are linked with aria-describedby, set aria-invalid and focus the first problem.

Form Page Validates
Contact ([data-contact-form]) Contact name, email, optional phone, interest, reply method, message of 10+ characters, consent
Booking ([data-booking] form) Book online session, time, name, email, mobile, all seven health questions, consent
One-rep max ([data-orm]) Training tools weight and reps

Connecting the contact form#

Use a form plugin (replace the form's Custom HTML block with the plugin's block; add the classes label, field and btn btn-primary to its labels, fields and button), or send the form from the script (in a child theme). In the "Contact form" part of assets/js/main.js, send it before showing the success state, for example with Formspree:

js
form.addEventListener('submit', function (e) {
  e.preventDefault();
  if (!validateFields($$('[data-validate]', form))) return;
  fetch('https://formspree.io/f/your-form-id', { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } })
    .then(function (res) {
      if (!res.ok) throw new Error('Send failed');
      // ...the existing code that hides the form and shows [data-form-success]
    })
    .catch(function () { toast(__('Sorry, that didn’t send. Please call 07700 900418.')); });
});

Update the success message in the contact-form pattern so it says the message was sent.

Connecting the booking flow#

Connect a real booking system rather than a form service. Many booking platforms offer an embeddable widget: replace the booking section with it (a Custom HTML block with its embed code) and keep the rest of the site. Or keep this flow and:

  1. Load real open times (through your own server, so API keys stay private) and return them from slotsFor() in the same { key, mins, coach } shape. Set BOOKING.demoBusyShare to 0.
  2. In the booking form's submit handler, POST the session, state.slot and the form data to your server.
  3. Show step 5 only after your system confirms, and change the confirmation copy to match.

The health answers are special category data under UK GDPR. Use HTTPS and a processor you have a data processing agreement with, keep the consent box, collect only what you need and state your retention period in your privacy notice. Don't store the answers in WordPress (for example with a plugin that saves form entries to the database) unless your hosting meets the same rules. They are never written to localStorage or cookies; keep it that way.

Saved data (localStorage)#

Key Shape Written by Validation on read
kinetic-goal JSON string, one of "strength", "fatloss", "mobility", "sport", "postnatal" Goal picker and goal filters Ignored unless it is a string that matches a GOALS key
kinetic-plan { "days": 3, "goal": "strength", "equipment": "gym", "minutes": 45 } Plan builder Every field is checked against PLAN_DAYS, GOALS, EQUIPMENT and 30/45/60; an invalid value is removed

Nothing else is stored; booking and contact details are never saved. To reset, use Reset on the plan builder or clear the site's storage in the browser's developer tools. Every read and write is wrapped in try/catch, so the site works when storage is blocked.

SEO and sharing#

Page titles come from WordPress: the page title, an em dash and the site name (for example "Pricing — Fenline Performance"); the home page shows the site name and tagline (Settings > General). The theme doesn't output meta descriptions or Open Graph tags; use an SEO plugin such as Yoast SEO, Rank Math or The SEO Framework, and give it assets/img/og-image.png (or your own 1200 × 630 image) as the default social image. WordPress already generates a sitemap at /wp-sitemap.xml. A studio benefits from ExerciseGym or LocalBusiness structured data (schema.org) with your real name, address, phone and opening hours; most SEO plugins can add it. The theme doesn't include it because it would describe a fictional studio.

Accessibility#

Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks (header, labelled navigation, main, footer), one h1 per page, labelled sections, visible focus outlines, the menu button with aria-expanded and a label that changes with its state, native radio groups for the goal picker, session, coach, time and health questions, filter chips with aria-pressed and a live count, live regions for the calculator, booking and plan builder, text summaries on every chart, native details for the FAQ, form errors announced through aria-describedby and aria-invalid, focus moved to the first error or the next step's heading, alt text or aria-hidden on every picture, and reduced-motion support for every animation. The demo passes axe (WCAG 2.1 A and AA).

When you edit: keep one Heading 1 per page and don't skip heading levels, give images alt text (or mark them decorative), keep link text meaningful, check contrast when you change colours (flare is only used behind dark text or as decoration), label any field you add to a form (<label for="...">) and add data-validate when it needs checking.

Building and deploying#

You only need this if you change the theme's files. The stylesheet assets/css/theme.css is compiled with Tailwind CSS from src/theme.css and the classes used in patterns/, parts/, templates/, inc/ and assets/js/:

sh
pnpm install
pnpm build   # compile assets/css/theme.css and assets/css/editor.css
pnpm watch   # recompile assets/css/theme.css on every change (run pnpm build before you commit)

Run pnpm build after adding a utility class to a pattern, template or the script's markup, otherwise the class has no style. Classes you type in the editor's Additional CSS class(es) field are only styled if the stylesheet already contains them.

To deploy, zip the theme folder (named freeframe-kinetic, without node_modules/) and upload it in Appearance > Themes, or copy the folder to wp-content/themes/ on your server. Serve the site over HTTPS, especially once the forms are connected.

Translations#

The theme is translation-ready with the text domain freeframe-kinetic. All text in patterns, templates and PHP goes through WordPress's translation functions; the script's strings (session types, coaches, goals, chart labels, exercises, booking lines, calculator lines, form errors and messages) are translated in PHP (freeframe_kinetic_script_strings() in inc/setup.php). Create a translation with Loco Translate, or with WP-CLI and Poedit:

sh
wp i18n make-pot wp-content/themes/freeframe-kinetic wp-content/themes/freeframe-kinetic/languages/freeframe-kinetic.pot

Save freeframe-kinetic-cy.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Kinetic setup is copied into your pages in the site's language at that moment; translate it in the editor like any other content. Dates and prices in the booking flow and the calculators are formatted in British English by the script (toLocaleDateString and toLocaleString with en-GB).

Child themes#

Use a child theme for changes you want to keep when Kinetic is updated (your hours, coaches, session types and prices in particular). Create wp-content/themes/kinetic-child/style.css:

css
/*
Theme Name: Kinetic Child
Template: freeframe-kinetic
*/

Templates, parts, patterns and theme.json in the child theme override the parent's files of the same name. To use your own copy of assets/js/main.js, dequeue the parent's script (freeframe-kinetic) and enqueue yours in the child's functions.php, keeping the inline window.freeframeKinetic strings and portrait URLs (or copy them). The parent's stylesheet only contains the classes the parent uses: if your child theme uses new utility classes, compile a stylesheet for the child (copy package.json and src/theme.css, point the @source lines at the child's folders) and enqueue it.

Troubleshooting#

  • The pages look unstyled or new classes do nothing. The stylesheet only contains classes it found when it was built. Run pnpm build after editing patterns, then reload.
  • "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Kinetic.
  • The week view says "No times left this week". Times need notice minutes of warning and are limited to coach shifts. Check COACHES[...].shifts, HOURS and CLOSED_DATES, and remember the demo hides about 45% of times (BOOKING.demoBusyShare).
  • Times or the open-now badge look an hour out. The site always uses STUDIO.timeZone. If your studio isn't in the UK, change it to your IANA zone (for example America/Toronto) and update STUDIO.zoneName and the "UK time" notes in the patterns.
  • A new programme or story doesn't show up under a filter. Its group needs the class ffd-goals--<goals> with goal ids joined by underscores. See Adding a programme or a client story.
  • The goal picker or plan builder opens with an old choice. It's restored from localStorage. Use Reset on the plan builder, or remove kinetic-goal / kinetic-plan in the browser's storage settings.
  • Home or Book online is missing from the desktop menu. The desktop header hides the menu's first and last links. See Navigation, header and footer.
  • Kinetic setup says the pages exist, but they are empty. It never overwrites existing pages. Delete (and empty the trash of) the pages with those slugs, or open each page and insert its page pattern from Patterns > Kinetic pages.

License#

MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme folder). Free for personal and commercial use, including client work. The athlete pictograms, coach portraits, studio illustration, map, icons, logo and social image are original to this template and included under the same license. Sofia Sans and Sofia Sans Extra Condensed are licensed under the SIL Open Font License 1.1 (OFL.txt in each folder of assets/fonts/). Fenline Performance, its coaches, clients, reviews and prices are fictional.

Search documentation

Search the guides and every template's documentation