Freeframe

Documentation

Vows

v1.0.4Updated Oct 18, 2026An elegant wedding website with RSVP, schedule, travel and registry.

Overview#

Vows is a warm, botanical wedding website built as a WordPress block theme, for a fictional couple, Amara & Theo, marrying at Hollowmere House on Saturday 19 June 2027. The theme has a home page with a live countdown to the ceremony, the couple's story as a timeline, a weekend schedule with add-to-calendar files, a travel page with an illustrated map, shuttle times and room blocks, an RSVP where guests find their invitation by code or name and reply for everyone on it, a registry with a honeymoon fund, a gallery with a lightbox, a searchable FAQ and a 404 page. It is version 1.0.1 (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/vows.

Everything you see is made of core WordPress blocks arranged in block patterns, so you edit text, links, buttons, lists and images in the block editor without touching code. Colours, the fonts and font sizes are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site follows. The parts that guests use (RSVP, registry, calendar files) read the wedding's data from assets/js/data.js, as in the HTML version.

Quick start#

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

  1. Download vows.zip from freeframe.dev/templates/vows (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 Vows.
  3. Go to Appearance > Vows setup and click Create the demo pages. This creates the Home, Our story, Schedule, Travel & stay, RSVP, Registry, Gallery and FAQ pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menus. Pages that already exist with the same slug are left alone, so it is safe to click it again.
  4. In Settings > General, set the Site Title to your names and the Tagline to the date: the home page's browser title is "Title · Tagline", the other pages' "Page · Title".
  5. Put in your own names, date, events, guest list and registry (see Content).

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 > Vows pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Vows.

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (names, date, countdown), save-the-date, plan-your-weekend, story-teaser, weekend-glance, rsvp-cta
Our story /story/ page-story.php story-header, story-timeline, story-quote
Schedule /schedule/ page-schedule.php schedule-header (days, whole-weekend calendar file), schedule-days
Travel & stay /travel/ page-travel.php travel-header, venue-map, getting-there (shuttle times), where-to-stay (booking codes), local-tips
RSVP /rsvp/ page-rsvp.php rsvp-header, rsvp-form
Registry /registry/ page-registry.php registry-header, honeymoon-fund, gifts
Gallery /gallery/ page-gallery.php gallery-header, gallery-photos, lightbox
FAQ /faq/ page-faq.php faq-header (search), faq-questions
404 any missing URL templates/404.html 404

WordPress 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 note, accommodation details), choose the Page with title template in the page's settings.

Project structure#

freeframe-vows/
  style.css                 theme header (name, version, requirements, license); no styles
  theme.json                design tokens: palette, fonts, font sizes, layout widths, button
  functions.php             loads the files in inc/
  inc/setup.php             stylesheet and scripts (with their strings, page and image URLs),
                            editor styles, the `js` class script, theme colour, favicon, title
                            separator, 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 > Vows setup: creates the demo pages and the menus
  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/data.js         the wedding's data: events, meals, guest list, registry, fund, gallery
  assets/js/main.js         the site's JavaScript
  assets/icons/             SVG icons for the Icon block
  assets/img/               botanical ornaments, gallery illustrations, favicon, social image
  assets/fonts/             Cormorant Garamond, Jost and Pinyon Script and their licenses (OFL.txt)
  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-vows.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 Used for
Base (paper), Linen, Blush, Card the page background, soft panels, icon circles and gift art, cards and fields
Contrast (ink), Muted, Line text, secondary text, hairlines
Sage, Sage deep, Sage light, Forest the botanical greens: eyebrows, the Sage button, the timeline, the footer
Terra, Terra light, Gold the terracotta accent (the RSVP button, script names, the current page), and the gold of the frames

The theme's stylesheet reads every colour from the palette (bg-forest, text-terra... are the presets), so changing Terra in Styles recolours every accent at once. The ornaments (assets/img/corner.svg, wreath.svg, sprig.svg) and the gallery illustrations carry their own colours: edit the SVG files to recolour them. Keep text colours at a 4.5:1 contrast.

Typography#

The fonts are Cormorant Garamond (headings, font-serif, roman 400 to 700 and italic 400 to 600), Jost (text, font-sans, 400 to 600) and Pinyon Script (the names, script), bundled in assets/fonts/ and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google. To use other fonts, install them in Appearance > Editor > Styles > Typography > Fonts (the Font Library) and select them, or replace the fontFace entries in theme.json. The utility classes use the families --wp--preset--font-family--jost, --wp--preset--font-family--cormorant-garamond and --wp--preset--font-family--pinyon-script, so rename those slugs only together with src/theme.css.

Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale). The stylesheet uses them too: changing Large in Styles changes every text-lg in the design.

Content#

Edit pages in Pages: click any text to change it, select a button to change its link, select an image and use Replace. 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 > Vows. Search for "Amara", "Theo", "Hollowmere" and "19 June" when you make it yours: they appear in the pages, the header and the footer.

Some content is in Custom HTML blocks, because blocks can't express it: edit those in the block's HTML view.

Pattern Custom HTML What it is
header the menu button a button driven by assets/js/main.js
home-hero the countdown and its "Today's the day" message see Countdown
save-the-date, schedule-header, schedule-days the "Add to calendar" buttons see Add to calendar
venue-map, getting-there, where-to-stay the illustrated map and its legend; the shuttle timetable; the copy buttons of the booking codes see Travel map and booking codes
rsvp-form the whole RSVP (the script builds the later steps) see RSVP
honeymoon-fund, gifts the progress bar and the contribution form; the category buttons, "Hide reserved gifts", the count and the gift list see Registry and honeymoon fund
gallery-photos, lightbox the photographs (buttons that open the viewer) and the viewer see Gallery
faq-header, faq-questions the search field; its result line see FAQ search
footer the small confirmation message ("toast") messages from the script

Blocks can't store data- attributes, so the theme writes them as classes on blocks and the first lines of assets/js/main.js turn them back into attributes before anything else runs: ffd-name--value becomes data-name="value", ffa-name--value becomes the attribute name="value" (ffa-aria-labelledby--fund-title, ffa-aria-hidden--true), and ff-hidden hides a block until the script shows it. On the FAQ page each question is a Details block with ffd-faq and each group of questions a section with ffd-faq-group; the search reads them.

The figure "£1,840 raised" on the registry is a <strong data-fund-raised> in the paragraph's text (edit it with Edit as HTML). Cards that link to a page ("Plan your weekend") are groups with the class link-card: the link is on the card's heading and covers the whole card. reveal fades a block in as it scrolls into view (classes like [--d:80ms] stagger neighbours).

The wedding's data (assets/js/data.js)

The parts guests use read window.VOWS in assets/js/data.js, the same file as in the HTML version:

Key Shape Used by
keepDemoCurrent true demo only: moves every date on a year once the weekend is over (see Demo dates below)
couple { a, b, names, date_long, date_short, venue, place, ceremony_iso, rsvp_by, hashtag, email } calendar file titles (names)
events { id, day, title, start, end, place, address, dress, body } .ics files
days { id, label, date, title } reference only (the schedule page is blocks)
meals, kidsMeal { id, label, note } RSVP dinner choices (kidsMeal is offered first to guests with child: true)
invites { code, label, plusOne, guests: { id, name, child? }[] } RSVP lookup
registry { id, name, category, price, needed, reserved, icon, note } gift list (icon is a key of giftIcons)
categories category names reference only (the filter buttons are in the gifts pattern)
fund { title, destination, goal, raised, amounts: { value, label }[] } honeymoon fund
gallery { slug, title, caption, shape } (shape is "tall" or "wide") lightbox
giftIcons { <name>: "<svg paths>" } gift card icons

Some values also appear in the pages, so change both together:

  • Ceremony date and time: couple.ceremony_iso, the data-countdown attribute in the countdown's Custom HTML block (home-hero), and the dates written on the pages, the header's mobile menu and the footer.
  • Schedule: each event's blocks on the Schedule page and its entry in events (used for the calendar files). Each "Add to calendar" button has data-ics="<event id>".
  • Fund: fund.goal and fund.raised, the "£1,840 raised of £3,000" text, the progress bar's aria-valuemax, aria-valuenow and width, and the amount buttons (data-amount).
  • Gallery: the photographs' Custom HTML block (each button's data-photo is its index in gallery, and its picture is assets/img/gallery-<slug>.svg) and the gallery list.

Add an invitation to invites. code is matched ignoring case, spaces and punctuation; id must be unique within the invitation:

js
{
  "code": "BIRCH-05",
  "label": "The Novak family",
  "plusOne": false,
  "guests": [
    { "id": "anna", "name": "Anna Novak" },
    { "id": "eli", "name": "Eli Novak", "child": true }
  ]
}

Add a gift to registry (needed is how many you'd like, reserved how many are already taken):

js
{ "id": "teapot", "name": "Stoneware teapot", "category": "Kitchen", "price": 48, "needed": 1, "reserved": 0, "icon": "pot", "note": "For long Sunday breakfasts." }

Times include their UTC offset (+01:00 is British Summer Time), so calendar files show the right local time for guests anywhere. Use your venue's offset on the wedding date.

data.js is public: anyone can read the guest list in the browser. For a real wedding keep the list short (codes and names only), or move the lookup to a server (see RSVP).

Demo dates. The demo is written for the weekend of 18–20 June 2027. While keepDemoCurrent in assets/js/data.js is true and that weekend is over (after the last event in events ends), the script moves every date to the same weekend of the next year, by whole weeks so weekdays never change (Saturday 19 June 2027 becomes Saturday 17 June 2028): the countdown, the events and calendar files, every <time datetime>, the text of data-date="2027-04-01" spans in the patterns ("1 April 2027"; with data-date-format="dm" "18 June"), data-edition="year" and data-edition="short" ("19.06.27"), and the date in the page title. Shifted dates are written in English. Before the weekend is over nothing changes. Set keepDemoCurrent to false once you enter your real dates.

Images and artwork#

All artwork is SVG in assets/img/: the botanical ornaments (corner.svg, wreath.svg, sprig.svg) and nine gallery illustrations (gallery-<slug>.svg, 1200 × 1500 for tall, 1200 × 900 for wide). In the pages they are Image blocks: select one and use Replace to pick a photo from the Media Library. The ornaments are decorative (alt=""). The gallery's photographs and the viewer read assets/img/gallery-<slug>.svg by the slugs in data.js; to use your photos there, replace the files (keeping the names) or change the paths in the gallery-photos block and in show() in assets/js/main.js. The favicon (assets/img/favicon.svg) is used until you set a Site Icon in Settings > General; assets/img/og-image.png (1200 × 630) is the social image to give your SEO plugin. Icons are Icon blocks: pick another icon from the Vows collection (the theme's icons) 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 initials ("A & T") are a paragraph with a link: edit them there.

The header has two Navigation blocks: the desktop menu ("Main menu": Our story, Schedule, Travel, Registry, Gallery, FAQ, with the RSVP button beside it) and the full-screen mobile menu ("Mobile menu": Home, the same pages and RSVP, each with an arrow drawn by CSS), both created by the setup. Edit them in Appearance > Editor > Navigation. WordPress marks the link to the current page. The header becomes solid once the page scrolls or the mobile menu opens.

The footer's menu is a List block in a navigation group; the confirmation message ("toast") the script shows is the small Custom HTML block after the footer.

Features#

Countdown#

The countdown (data-countdown="2027-06-19T15:00:00+01:00", role="timer") shows days, hours, minutes and seconds to the ceremony and updates every second. On the day it stops at zero and shows "Today's the day" (data-countdown-done). Change the date in the block's HTML and in couple.ceremony_iso.

Add to calendar#

Buttons with data-ics="<event id>" download an .ics file for that event, data-ics="all" one for the whole weekend. The files are built in the browser from events (title with the couple's names, place, description and dress code, times in UTC), escaped and folded as RFC 5545 requires, and work with Apple Calendar, Google Calendar and Outlook. A message confirms the download.

RSVP#

  1. Find your invitation: the guest types the code on their card (ROSE-24) or their full name as it is written there; case, spaces, punctuation and accents don't matter.
  2. Your reply: for each guest on the invitation, attending or not, a dinner choice (children get the little ones' plate first) and dietary needs; a plus-one with their name and meal when the invitation allows one; a song request, a note and an email for the confirmation. Missing answers are listed at the top and marked on each field, and focus moves to the first one.
  3. Review, then Send reply.

An unsent reply is saved on the device (localStorage, key vows:rsvp-draft:v1) as the guest types, and restored when they find the same invitation again. Saved replies are validated when read: unknown invitations, guests, answers and meals are dropped, text is cut to its maximum length and the stored value is repaired. Nothing is sent: see Connecting the forms.

Registry and honeymoon fund#

The gift list is built from registry: the category buttons filter it, "Hide reserved gifts" hides the gifts that are fully reserved, and "Reserve" marks a gift as reserved by this visitor (remembered in the browser, key vows:reserved:v1, validated when read: unknown, duplicate or fully reserved gifts are dropped). The fund form takes a suggested amount or one of £5 to £5,000 and a name, then updates the total and the progress bar. Both are demos: reservations aren't shared between guests and no payment is taken; link the fund to your payment page or gift list provider.

Travel map and booking codes#

The legend buttons beside the map (data-pin-target) highlight a place on the map (data-pin); selecting it again clears it, and the pins can be selected too. The booking codes' copy buttons (data-copy) copy the code and confirm with a message.

Each photograph opens the viewer (a native <dialog>): the arrow keys and the Previous/Next buttons move between photos, Escape, the close button or a click outside close it, and focus returns to the photograph you opened.

Typing in the search field shows only the questions that contain the words, opens them, hides empty groups and announces "2 questions match …"; clearing it restores the questions you had open.

Header, mobile menu, scroll reveals and back to top#

  • The header becomes solid once the page scrolls (is-scrolled).
  • The menu button (data-menu-toggle) opens the full-screen #mobile-menu, locks the page's scrolling and swaps its icon; Escape closes it and returns focus to the button.
  • Blocks with the reveal class fade in as they scroll into view; everything above the bottom of the screen is revealed too. With reduced motion, or without JavaScript, everything shows at once.
  • A "Back to top" button appears after about one screen of scrolling and steps aside while the mobile menu is open; it scrolls to the top (instantly with reduced motion) and moves focus to the main content. To remove it, delete the "Back to top" part of assets/js/main.js and the .back-to-top styles in src/theme.css, then run pnpm build.

Block styles#

In the block settings' Styles panel:

Block Styles
Button Fill (default, terracotta), Sage, Outline, Outline (on dark)

WordPress's own Outline style is removed in the editor. The FAQ's questions are Details blocks; the round plus that turns into a cross is drawn by CSS (ff-plus).

Connecting the forms#

Form Page Checks
RSVP RSVP an invitation code or name; attendance and a dinner for every guest coming; a plus-one's name and dinner; an email
Honeymoon fund Registry a suggested amount or £5 to £5,000; a name

Both are demos: nothing leaves the browser. To receive RSVPs, send the reply to a form service (Formspree, Basin...) or a Google Sheet (with Apps Script) where the script says "Demo only", in a child theme's copy of assets/js/main.js:

js
fetch('https://formspree.io/f/your-id', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  body: JSON.stringify(state)
});

state holds the invitation code, every guest's answers, the plus-one, the song, the note and the email. Then update the thank-you text, which says the reply was not sent. For the fund, point the form at your payment or gift list page. A form plugin (WPForms, Gravity Forms, Fluent Forms...) can replace either form's Custom HTML block if you prefer a simpler reply form.

Saved data (localStorage)#

Key What Validation
vows:rsvp-draft:v1 the unsent RSVP reply dropped if not an object or for an unknown invitation; unknown guests, answers and meals removed; text cut to length; repaired in place
vows:reserved:v1 gifts this visitor reserved non-lists, unknown, duplicate and fully reserved ids removed; repaired in place

Unreadable values are removed. Private browsing or full storage simply keeps the data in memory.

SEO and sharing#

Page titles come from WordPress: the page title, a middle dot and the site name (for example "Schedule · Amara & Theo"); 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. A wedding site is often better kept out of search engines: tick Settings > Reading > Discourage search engines from indexing this site, and consider a password on the RSVP page (Page settings > Visibility).

Accessibility#

Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks (header, labelled navigations, main, footer), one h1 per page, visible focus outlines, the mobile menu with aria-expanded, a countdown with role="timer" and a label, RSVP steps marked aria-current="step" with headings that take focus, an error summary and per-field errors (aria-invalid, aria-describedby), radio groups with labels, filter buttons with aria-pressed, live regions for counts and messages, a progress bar with values, a native modal lightbox with focus return, decorative ornaments hidden from screen readers, 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, give pictures alt text (or mark them decorative), keep link text meaningful, and label any field you add to a form.

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 or template, 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-vows, without node_modules/) and upload it in Appearance > Themes, or copy the folder to wp-content/themes/ on your server.

Translations#

The theme is translation-ready with the text domain freeframe-vows. All text in patterns, templates and PHP goes through WordPress's translation functions; the script's strings and the visible text of data.js (events, meals, gifts, the fund, photos) are translated in PHP (freeframe_vows_script_strings() in inc/setup.php), while ids, invitation codes, names and gift categories (the filters' values) stay as they are. Create a translation with Loco Translate, or with WP-CLI and Poedit:

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

Save freeframe-vows-fr_FR.mo (for example) in wp-content/languages/themes/ or in the theme's languages/ folder. Page content created by Vows setup is copied into your pages in the site's language at that moment; translate it in the editor like any other content.

Child themes#

Use a child theme for changes you want to keep when Vows is updated. Create wp-content/themes/vows-child/style.css:

css
/*
Theme Name: Vows Child
Template: freeframe-vows
*/

Templates, parts, patterns and theme.json in the child theme override the parent's files of the same name. The parent's stylesheet only contains the classes the parent uses: if your child theme's patterns use 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. Your wedding's data.js belongs in the child theme too: load it in place of the parent's (wp_deregister_script( 'freeframe-vows-data' ) and wp_register_script( 'freeframe-vows-data', … ) on wp_enqueue_scripts, priority 30), so updates never overwrite your guest list. To change the script, do the same with freeframe-vows and assets/js/main.js, keeping the data the parent prints before it (window.freeframeVowsL10n, window.freeframeVowsPages, window.freeframeVowsAssets).

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 > Vows.
  • A guest can't find their invitation. The code or the full name must match an entry in invites in data.js (accents, case and punctuation are ignored).
  • A calendar file has the wrong time. Check the UTC offset of the times in events.
  • The countdown shows zeros. The date in data-countdown has passed; on the day it shows "Today's the day".
  • The header menu shows other pages, or an old menu. Select each Navigation block and choose "Main menu" or "Mobile menu" (or yours) in the block settings.
  • Vows 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 > Vows pages.
  • RSVPs don't arrive. The form is a demo; see Connecting the forms.

Differences from the HTML version#

The pages, copy, design and interactions are the same. What WordPress changes:

  • Straight apostrophes typed in text blocks are shown curly, as WordPress does everywhere (text in Custom HTML blocks, the script's messages and menu labels keeps them straight).
  • The "Plan your weekend" cards have the link on their heading, stretched over the card (the HTML version wraps the whole card in a link), so screen readers announce the heading as the link's name.
  • Scroll reveals hide content only once JavaScript runs (js class), where the HTML version swaps a no-js class on <html>: the same result.
  • Data and ARIA attributes on content blocks are written as classes and restored by the script (see Content).

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 the illustrations. Cormorant Garamond, Jost and Pinyon Script are licensed under the SIL Open Font License 1.1 (assets/fonts/*/OFL.txt). Amara, Theo and everyone in this theme are fictional.

Search documentation

Search the guides and every template's documentation