Freeframe

Documentation

Bistro

v1.0.1Updated Oct 9, 2026A warm, high-end website for a restaurant, café or wine bar.

Overview#

Bistro is a warm, high-end website for a restaurant, café or wine bar, built as a WordPress block theme. The demo belongs to Olive & Ember, a fictional wood-fired Mediterranean kitchen in an old town. The theme has a home page with a wood-fire hero, a live "Open now" badge, signature dishes, a chef quote, reviews and opening hours, a menu page with lunch, dinner, drinks and dessert tabs and a vegetarian, vegan and gluten-free filter, an "Our story" page with a timeline, values, team and suppliers, a reservations page whose time slots follow the opening hours, a private dining page with three packages, upcoming events and an enquiry form, a contact page with an illustrated map and a FAQ, and a 404 page. It is a light design in cream, with dark page headers, ember and olive accents. 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/bistro.

The pages are made of core WordPress blocks arranged in block patterns, so you edit text, dishes, prices, links, buttons and images in the block editor without touching code. The opening hours are set once in assets/js/main.js and drive the open/closed badges, today's highlighted row and the reservation times. Colours, the two fonts and font sizes are design tokens in theme.json: change them in Appearance > Editor > Styles and the whole site follows.

Quick start#

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

  1. Download bistro.zip from freeframe.dev/templates/bistro (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 Bistro.
  3. Go to Appearance > Bistro setup and click Create the demo pages. This creates the Home, Menu, Our story, Reservations, Private dining & events and Contact & directions pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the two 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 (the demo uses "Olive & Ember"; the header and footer logo show it) and the Tagline. Use pretty permalinks (Settings > Permalinks, any option except Plain): the theme's buttons and links point at addresses such as /reservations/ and /menu/#drinks.
  5. Set your opening hours and time zone (see Opening hours). Then 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), sets the site title to "Olive & Ember" and the tagline to "Wood-fired kitchen in the Old Town", switches on pretty permalinks and creates the demo pages (blueprint.json). 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 > Bistro pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Bistro.

Page Slug Page pattern Sections (pattern files in patterns/)
Home / page-home.php home-hero (fire artwork, headline, open status, address and rating strip), home-story (text, three numbers, two pictures), signature-dishes (four dishes), chef-quote (portrait and quote), reviews (three reviews with a star rating), visit (opening hours with today highlighted, booking call to action)
Menu /menu/ page-menu.php menu-hero, menu-tabs (Lunch / Dinner / Drinks / Desserts tabs and the dietary filter), allergens (badge legend and allergy notes, anchor #allergens)
Our story /about/ page-about.php about-hero, about-story (timeline), about-values (fire, season, neighbourhood), team (four illustrated portraits, anchor #team), suppliers, about-cta
Reservations /reservations/ page-reservations.php reservations-hero, reservation-form (booking form, opening hours, "Good to know")
Private dining & events /events/ page-events.php events-hero, spaces (three packages), upcoming (four events, anchor #upcoming), enquire (enquiry form, anchor #enquire)
Contact & directions /contact/ page-contact.php contact-hero, contact-details (address, phone, email, transport, opening hours, map), contact-form (contact form and FAQ)
404 any missing URL templates/404.html 404

Every page has the same header and footer; the footer holds the opening status and a newsletter sign-up form. Posts use templates/single.html; the blog, archives and search results use templates/index.html, templates/archive.html and templates/search.html.

Page headers#

Every page starts with a dark page header (the *-hero patterns): the first section of the page, with the class ff-hero. The site header is fixed at the top and transparent over it, with light text. In the HTML version the inner pages' headers sit between <header> and <main>; in WordPress they are the first section of the page content (inside <main>), so you edit them with the rest of the page.

Pages without an ff-hero section (posts, archives, search results and plain pages you write yourself) get the header's dark background from the start and space at the top of the content, so nothing hides under the header (a :has() rule in src/theme.css). When you build a new page in the theme's style, start it with one of the page-header patterns (Patterns > Bistro: "Menu: page header", "Our story: page header"... ) and change its text, or give your own first section the class ff-hero (Block settings > Advanced > Additional CSS class(es)) if it has a dark background.

Project structure#

freeframe-bistro/
  style.css                 theme header (name, version, requirements, license); no styles
  theme.json                design tokens: palette, fonts, font sizes, spacing, layout widths, button;
                            the custom template and the template parts
  functions.php             loads the files in inc/
  inc/setup.php             stylesheet and script, the script's translations, the `js` class, editor
                            styles, head meta (theme colour, favicon), title separator, pattern
                            categories
  inc/block-styles.php      the Light (on dark), Outline (on dark) and Icon only button styles
  inc/icons.php             registers assets/icons/*.svg for the Icon block
  inc/icon-labels.php       the icons' names shown in the editor
  inc/site-setup.php        Appearance > Bistro setup: creates the demo pages and the two 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/main.js         the site's JavaScript: opening hours, header and mobile menu, reveals,
                            menu tabs and dietary filter, forms and reservation slots, back to top
  assets/icons/             SVG icons for the Icon block (check, envelope, farm, fish, flame,
                            location, map-pin, phone, quote, rating-5, sprout, star, tram)
  assets/img/               dish, chef, fire and wine illustrations, favicon, social image
  assets/fonts/             Cormorant Garamond and Manrope files 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; none used)
  src/editor-css.mjs        writes assets/css/editor.css after each build
  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

assets/js/main.js starts with a short "WordPress:" adapter, then the HTML template's script in five numbered parts marked by comment banners: 1. Opening hours, 2. Header + mobile menu, 3. Scroll reveal, 4. Tabs + dietary filter and 5. Forms, and finally the back-to-top button.

Customising#

Brand and colours#

The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles > Colors > Palette. The stylesheet reads them through Tailwind tokens (in src/theme.css), so a change in Styles recolours every block that uses the token:

Colour (slug) Tailwind token Used for
Cream (base) cream page background, light text on dark sections
Char (contrast) char main text, page headers, dark bands, the footer, selected tabs
Parchment (parchment) parchment soft panels (allergens, empty states, notes)
Sand (sand) sand borders, dividers, the dashed lines between dishes
Char 2 (char-2) char-2 cards on dark backgrounds, picture backgrounds, the newsletter field
Char 3 (char-3) char-3 not used by the demo's classes (the page-header glows write its value, #33261d, out)
Ember (ember) ember primary buttons, prices, eyebrows, links, form errors
Ember dark (ember-dark) ember-dark primary button and link hover
Ember light (ember-light) ember-light the "Closed" dot, errors on dark backgrounds
Olive (olive) olive menu group headings, the vegan badge, thank-you ticks
Olive light (olive-light) olive-light the vegetarian badge
Gold (gold) gold eyebrows on dark backgrounds, "Today" tags, focus rings, the gluten-free badge
Muted (muted) muted secondary text
White (white) (Tailwind's white) cards, form fields, button text

For a deep blue accent instead of ember, change Ember, Ember dark and Ember light in Styles, or in theme.json:

json
{ "slug": "ember", "name": "Ember", "color": "#2b4c7e" },
{ "slug": "ember-dark", "name": "Ember dark", "color": "#1f3a63" },
{ "slug": "ember-light", "name": "Ember light", "color": "#7c9ccf" }

Bistro has no dark mode: it is a light page with dark page headers and a dark footer. Some colours are written outside the palette, so change them by hand if you change the dark or accent colours:

  • the header's dark background when scrolled or on pages without a page header, rgb(23 17 13 / 0.92) in .site-header.is-scrolled and body:not(:has(.ff-hero)) .site-header (src/theme.css), and the browser UI colour printed by freeframe_bistro_head_meta() in inc/setup.php (<meta name="theme-color" content="#17110d">);
  • the green "Open now" dot (#4ade80 in [data-state="open"] .status-dot) and the gluten-free badge text (#664a12 in .badge-gf) in src/theme.css;
  • the glows in the page headers, written as classes on empty decorative Groups (for example bg-[radial-gradient(ellipse_at_80%_110%,#8a3216_0%,transparent_55%),...]);
  • the illustrations in assets/img/, the team portraits (inline SVG in the team pattern) and the contact map (inline SVG in contact-details).

Typography#

Two fonts are bundled in assets/fonts/ (variable files, one per character subset) and declared in theme.json (settings.typography.fontFamilies), so no request goes to Google:

Font Slug Used for
Manrope (weights 400 to 700) manrope all text (the site's Text font)
Cormorant Garamond (500 to 700, italic 500 to 600) cormorant-garamond headings, eyebrows, prices, the mobile menu (the font-display class)
Monospace (system fonts) mono code in posts

To use another font, install it in Appearance > Editor > Styles > Typography > Fonts (the Font Library) and select it for Text, or replace the fontFace entries in theme.json. The theme's utility classes use the fonts through their slugs (--font-sans is --wp--preset--font-family--manrope, --font-display is --wp--preset--font-family--cormorant-garamond in src/theme.css), so rename a slug only together with src/theme.css. The contact map's labels name "Cormorant Garamond" and "Manrope" in their SVG; change them in the map's Custom HTML block.

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. Headings take their size from classes on the block (the home headline is text-6xl sm:text-8xl): change them in Additional CSS class(es).

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 > Bistro. Bistro setup copies the patterns into the pages it creates, so later changes to the pattern files don't change your pages.

The restaurant's name, address ("12 Harbour Street, Old Town, Porto Velho"), phone numbers (+1 (555) 014-2020 and the events line +1 (555) 014-2021), email addresses (hello@oliveandember.example, events@oliveandember.example) and staff names appear in many places: the header's mobile menu, the footer (patterns/footer.php, including the copyright line "© 2026 Olive & Ember..."), the home hero strip, the reservations, events and contact pages, the map and the FAQ. Search for them when you rebrand.

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

Pattern Custom HTML
header the menu button
menu-tabs the tab buttons, the dietary filter buttons
reservation-form the reservation form, its thank-you panel
enquire the private dining enquiry form, its thank-you panel
contact-details the map
contact-form the contact form, its thank-you panel
team the four portraits (inline SVG)
footer the newsletter form, its thank-you message

WordPress turns straight apostrophes and quotes typed in normal text blocks into curly ones (wptexturize), so "doesn't" shows as "doesn’t", where the HTML version keeps the straight apostrophe.

Opening hours#

The opening hours are set once, at the top of part 1. Opening hours of assets/js/main.js, as in the HTML version:

js
// Opening hours per weekday (0 = Sunday). Times are minutes after midnight.
// null = closed all day.
var HOURS = {
  0: { open: 12 * 60, close: 16 * 60 },
  1: null,
  2: { open: 17 * 60, close: 22 * 60 },
  3: { open: 17 * 60, close: 22 * 60 },
  4: { open: 17 * 60, close: 22 * 60 },
  5: { open: 12 * 60, close: 23 * 60 },
  6: { open: 12 * 60, close: 23 * 60 }
};
var LAST_SEATING_BEFORE_CLOSE = 90; // minutes
var SLOT_STEP = 30;                 // minutes between reservation slots

and a few lines below:

js
var TIMEZONE = 'America/New_York';
Setting Meaning
HOURS One entry per weekday (0 = Sunday, 6 = Saturday): opening and closing time in minutes after midnight (17 * 60 + 30 is 5:30pm), or null for a closed day. One opening period per day.
LAST_SEATING_BEFORE_CLOSE The last bookable time, in minutes before closing
SLOT_STEP Minutes between bookable times
TIMEZONE The restaurant's IANA time zone, for example 'Europe/London' or 'America/Chicago'

They drive the open/closed badges, today's highlighted row and the reservation time slots (see Features). "Now" is always read in TIMEZONE, so every visitor sees the same status and bookable times, wherever their device is.

The hours printed on the pages are text in the patterns, not generated: update them to match.

Pattern Printed hours
visit (Home), reservation-form (sidebar), contact-details the opening hours rows
menu-tabs the lines under the Lunch tab ("Friday to Sunday, 12pm – 4pm") and the Dinner tab
reservation-form "Last seating 90 minutes before closing."

Each opening hours row is a Group (class hours-row) with the day paragraph and the hours paragraph. Its class ffd-day--2,3,4 lists the weekdays the row covers (0 = Sunday), so the script can mark today's row: it gets bold text and the gold "Today" tag (a span inside the day text, shown only on today's row). To regroup the days, change the row's text and that class in Advanced > Additional CSS class(es), for example ffd-day--1,2,3,4 for "Monday – Thursday". Duplicate a row to add one.

The script is part of the theme, so make these changes in a child theme to keep them through theme updates (see Child themes).

The menus are on the Menu page, in the menu-tabs pattern. In List View the structure is:

Group (ffd-tabs)
  the tab buttons (Custom HTML) and the dietary filter (Custom HTML)
  Group
    Group, HTML anchor "lunch" (a tab panel)
      the panel's hours line
      Group (the grid)
        Group (ffd-menu-group): Heading 3 "Flatbreads & sandwiches", then a list of dishes
          Group as <li> (ffd-dish ffd-diet--v): a dish
      the "Nothing in this section matches..." message (ffd-empty ff-hidden)
    Group, HTML anchor "dinner" ... "drinks" ... "desserts"

Each dish is a Group with the classes ffd-dish and ffd-diet--<codes>, holding the name (Heading 4), the badges (small paragraphs with the classes badge badge-v, badge badge-vg or badge badge-gf), the description and the price (a paragraph, for example "9").

To add a dish: select a dish in List View (or click its name, then Select parent until the <li> Group is selected), choose Duplicate in the block toolbar's options, then change the name, description and price. Copy a dish that already has the badges you need, or delete the badge paragraphs it shouldn't have. Then set its dietary codes in Advanced > Additional CSS class(es):

Class Dish
ffd-diet--v vegetarian
ffd-diet--vg vegan (also shown by the Vegetarian filter)
ffd-diet--gf gluten-free
ffd-diet--v_gf vegetarian and gluten-free
ffd-diet--vg_gf vegan and gluten-free
ffd-diet none of these

The underscore stands for a space: ffd-diet--v_gf is the HTML version's data-diet="v gf". Keep the ffd-dish class on every dish; without it the filter ignores the dish. The badges are what guests see, the class is what the filter reads, so keep the two in step.

To add a group (a heading with its own list), duplicate an ffd-menu-group Group and change its heading and dishes. Drinks and desserts work the same way: the filter applies to every tab.

Private dining packages#

The three packages on the events page (spaces) are cards with a picture, guests, name, description, a list of what's included and an Enquire button. Each button's Buttons block has a class naming its package, for example ffd-package--The_Cellar_Room: clicking it jumps to the enquiry form (#enquire) and selects "The Cellar Room" in its Space field.

The value (with underscores for spaces) must match an option of the Space select exactly. The options are in the enquiry form's Custom HTML block (enquire):

html
<option>The Cellar Room</option><option>The Long Table</option><option>Whole restaurant</option><option>Not sure yet</option>

To rename a package or add a fourth, change the card, the option and the class together. The options have no value attribute, so their text is their value; on a translated site, give the options a fixed value (for example <option value="The Cellar Room">) so the buttons still match.

The upcoming events (upcoming) are plain text rows with a date, a title, a description and a Book seats button that links to the Reservations page; edit them like any other text.

Images and artwork#

All the pictures are original SVG illustrations in assets/img/, shown with Image blocks:

File Where Shape
hero-fire.svg (1600 × 1000) the home hero background, the "Our story" timeline wide; cropped to fill
dish-flatbread.svg, dish-octopus.svg, dish-lamb.svg, dish-bream.svg (800 × 800) signature dishes (4:5 frames), private dining packages (16:10 frames) square, cropped to fill
dish-burrata.svg, wine.svg (800 × 800) the home page's kitchen section square
dish-dessert.svg (800 × 800) not used on the demo pages square
chef.svg (600 × 750) the chef quote 4:5
favicon.svg the logo in the header and footer, and the browser icon square

To use photos, select an Image block and use Replace with a picture from your Media Library. Keep similar proportions (the frames crop to fill), and give each photo alt text, or leave the alt empty when it is decorative (the home hero's fire is).

Other images:

  • Team portraits on the Our story page are inline SVG drawings in Custom HTML blocks. To use a photo, delete the Custom HTML block and insert an Image block in its place, set its Aspect ratio to Square and give it the classes overflow-hidden rounded-[2rem].
  • Contact map: an inline SVG in a Custom HTML block (contact-details), with a title and description for screen readers. Replace the block with an Image block or a map embed for a real map.
  • Icons (the contact cards, the suppliers, the quote mark, the star rating, the package ticks) are Icon blocks: select one and choose another icon from the Bistro collection.
  • Logo: the header and footer show assets/img/favicon.svg (an Image block) next to the Site Title block. Replace the image in Appearance > Editor > Patterns > Header (and Footer); the name comes from Settings > General.
  • Social image: assets/img/og-image.png (1200 × 630, source og-image.svg). WordPress doesn't output Open Graph tags by itself; see SEO and sharing.
  • Favicon: assets/img/favicon.svg is used until you set a Site Icon in Settings > General.

WordPress doesn't allow SVG uploads by default; upload your replacements as JPG, PNG or WebP.

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 holds the logo, the desktop menu and a Reserve a table button (from 1024 px wide) and, below 1024 px, the menu button that opens the mobile menu (#mobile-nav: the mobile menu links, a Reserve a table button, the open status and the phone number). It has two Navigation blocks with two menus, both created by Bistro setup:

Menu Links Shown
Main menu Menu, About, Events, Contact in the header from 1024 px wide
Mobile menu Home, Menu, About, Events, Contact in the mobile menu below 1024 px

The links point at the pages, with the short labels the HTML template uses ("About" for Our story, "Events" for Private dining & events, "Contact" for Contact & directions, from freeframe_bistro_menu_label() in inc/site-setup.php). Reservations isn't in the menus: the Reserve a table buttons link to it.

Edit the menus in Appearance > Editor > Navigation. To add a page to the site's navigation, add it to both menus. The header pattern points each Navigation block at its menu by id (freeframe_bistro_menu_ref() in inc/site-setup.php reads the ids saved in the freeframe_bistro_menus option). If a menu doesn't exist, the Navigation block shows the most recently created menu; select the block and choose the menu in the block settings. WordPress marks the current page's link with aria-current="page" (a gold underline on desktop, gold text in the mobile menu); the script does the same for the header's Reserve a table buttons on the Reservations page.

The footer has the logo, a short description and social links, a Visit column (address, phone, email and the open status), an Explore list of links, the newsletter form ("The Ember Letter") and the legal line. The social links are Button blocks with the Icon only style and the class icon-before-instagram, icon-before-facebook or icon-before-tiktok, which draws the icon; their text (Instagram, Facebook, TikTok) is hidden but still read by screen readers. They point at # in the demo: select each button and set your profile's address. The Explore links are a List block: click a link to change it, press Enter to add one.

The copyright line prints the current year when the footer comes from the theme's files. After you edit and save the footer in the Site Editor, the year is stored as text; update it there each year.

The links written in the patterns (buttons, the footer, "Groups of nine or more: see private dining") are addresses such as /reservations/, /events/#enquire, /menu/#allergens and /about/#team. If you change a page's slug, update them too.

Classes the script reads#

Blocks keep classes, not data-* or ARIA attributes, so the theme writes the HTML template's attributes as classes: data-x="y" is the class ffd-x--y (ffd-x when the attribute is empty; an underscore in the value stands for a space), hidden is ff-hidden, and role, aria-*, tabindex and title on block-level elements are ffa-<name>--<value>. The adapter at the top of assets/js/main.js turns them back into attributes before anything else runs. You see them in Block settings > Advanced > Additional CSS class(es); keep them when you edit.

Class Where What it does
ffd-header the header scroll background, mobile menu
ffd-open-status, ffd-open-text a paragraph and the span inside it, in home-hero, visit, reservation-form, the header's mobile menu and the footer the open/closed status
ffd-day--2,3,4 opening hours rows today's row
ffd-tabs the Group around the menus the tabs
ffa-role--tabpanel ffa-aria-labelledby--tab-lunch ffa-tabindex--0 the tab panels (HTML anchors lunch, dinner, drinks, desserts) tab panel semantics
ffd-menu-group, ffd-dish, ffd-diet--v_gf menu groups and dishes the dietary filter
ffd-empty one message per tab panel shown when the filter leaves a tab empty
ffd-filter-status ffa-aria-live--polite a hidden paragraph below the filter announces the filter result
ffd-package--The_Cellar_Room the Enquire buttons preselects the enquiry form's Space
ffd-reveal, [--d:80ms] many blocks scroll reveal and its delay
ffa-aria-hidden--true the decorative glows in page headers hidden from screen readers
ffa-aria-labelledby--visit-title some sections names the section after its heading
ff-hidden the mobile menu, the Dinner, Drinks and Desserts panels, the empty messages hidden until the script shows it

Theme classes the stylesheet reads: ff-hero (see Page headers), hours-row and today-tag, badge-v, badge-vg and badge-gf, link-card (the logo: the site title's link covers the image too), ff-icon-plus (the FAQ's "+"), the icon-before-* social icons and the button padding classes (see Buttons).

In the editor, blocks with ff-hidden (the Dinner, Drinks and Desserts panels, the empty messages, the mobile menu) are shown with a dashed outline so you can edit them; keep the class, so the page doesn't show every menu for a moment while it loads.

Features#

Header and mobile menu#

The header is fixed at the top and transparent over the page header; once the page scrolls (24 px) it gets a dark blurred background (is-scrolled, set by assets/js/main.js). Below 1024 px the menu button (data-nav-toggle) opens #mobile-nav below the header: it updates aria-expanded and its label ("Open menu" / "Close menu"), swaps its icon, stops the page from scrolling behind it and moves focus to the first link. It closes when you choose a link, on Esc (returning focus to the button) and when the window grows past 1024 px.

Open now badge and today's hours#

Every paragraph with the class ffd-open-status shows the live status from HOURS in its ffd-open-text span: "Open now · until 10pm", "Closed · opens today at 5pm", "Closed · opens tomorrow at 5pm" or "Closed · opens Tuesday at 5pm". It also gets data-state="open" or "closed", which colours the dot (green or ember). Opening hours rows whose ffd-day-- list contains today get the is-today class (bold, with the "Today" tag). Both use the time in TIMEZONE and refresh every minute. Without JavaScript the paragraph keeps its placeholder text ("See opening hours").

The tab bar (a Custom HTML block in menu-tabs) follows the WAI-ARIA tabs pattern: buttons with role="tab", aria-selected and aria-controls naming a panel's HTML anchor. Left/Right arrows move between tabs and Home/End jump to the first or last. Clicking a tab puts its panel in the address (/menu/#drinks), and opening /menu/#drinks selects the Drinks tab, so you can link to a menu directly (the signature dishes link to /menu/#dinner).

To add a tab, add a button to the tab list:

html
<button type="button" role="tab" id="tab-brunch" class="tab" aria-selected="false" aria-controls="brunch" tabindex="-1">Brunch</button>

then duplicate a panel Group, set its HTML anchor (Advanced) to brunch, change its class ffa-aria-labelledby--tab-lunch to ffa-aria-labelledby--tab-brunch (keep ffa-role--tabpanel ffa-tabindex--0 and ff-hidden) and replace its dishes.

Dietary filter#

The filter buttons (data-diet-filter: all, v, vg, gf, with aria-pressed) show only the matching dishes, in every tab. Vegan dishes also count as vegetarian. Menu groups with no matching dish are hidden, a tab that is left empty shows its "Nothing in this section matches..." message, and the hidden status paragraph announces the result to screen readers ("Showing 14 matching dishes and drinks."). See Menu dishes for the classes a dish needs.

Reservation time slots#

On the Reservations page the Time select is filled from the chosen date (setupReservation() in part 5 of assets/js/main.js):

  • Times run every SLOT_STEP minutes from opening until LAST_SEATING_BEFORE_CLOSE minutes before closing; a note under the field shows that day's hours ("Thursday: kitchen open 5pm–10pm. Last seating 8:30pm.").
  • For today, only times at least an hour from now are offered. When none are left, the date field explains "There are no more tables today."
  • On a closed day (null in HOURS) the time shows "Closed that day" and the date field explains "We are closed on Mondays."
  • The date can't be in the past: its minimum is today in TIMEZONE (the date fields of both the reservation and the enquiry form have data-min-today). A reservation can't be more than two months ahead: data-max-months="2" sets the field's maximum.
  • Choosing "9 or more" guests shows a note pointing to the private dining enquiry (data-party-note).
  • The seating choice (Dining room, Counter by the fire, Terrace) is a set of radio buttons drawn as cards; the arrow keys move between them and the chosen card is outlined.

Character counter#

The "Allergies or special requests" field (maxlength="300", data-count) shows "12 / 300" in the element whose id is the field's id plus -count (res-notes-count), announced politely to screen readers.

Scroll reveal#

Blocks with the ffd-reveal class fade and rise when they enter the viewport. A delay is a class such as [--d:80ms]; the stylesheet contains 0ms, 60ms, 70ms, 80ms, 90ms, 100ms, 120ms, 140ms, 150ms, 160ms, 180ms, 200ms, 210ms, 240ms, 270ms and 300ms (other values need pnpm build). Content is only hidden once JavaScript runs (the js class that freeframe_bistro_theme_script() adds to <html> first in <head>). With reduced motion everything is shown immediately, and the flickering glow in the page headers stops. In the editor everything is always visible.

Frequently asked questions#

The questions on the contact page (contact-form) are core Details blocks: click the summary to edit the question, and the paragraph inside for the answer; duplicate one to add a question. They open with the keyboard and screen readers like any <details> element. The round "+" that turns into a "×" is a span with the class ff-icon-plus at the end of the summary text, drawn with CSS.

Back to top#

A button appears after scrolling about one screen down; it scrolls to the top (instantly with reduced motion), moves keyboard focus to the main content and lifts itself above toasts. Add data-back-to-top-avoid to anything you pin to the bottom of the screen (a cookie bar, a chat launcher) and the button sits above it. It is created by assets/js/main.js; to remove it, delete the "Back to top" part of that file and the .back-to-top styles in src/theme.css, then run pnpm build.

Buttons#

The Button block's default style is the primary ember button. In the block settings' Styles panel there are also (registered in inc/block-styles.php):

Style Looks like
Outline (WordPress's own, restyled) a dark outline button for light backgrounds
Light (on dark) a cream button for dark sections
Outline (on dark) a cream outline button for dark sections
Icon only a round icon button whose text is only read by screen readers (the footer's social links)

Some buttons carry padding classes (Advanced > Additional CSS class(es)) that stand for the HTML template's: ff-btn-px-7 (px-7), ff-btn-py-35 (py-3.5) and ff-btn-py-2 (py-2).

Forms#

Every form is a Custom HTML block marked data-validate="<kind>" that names its thank-you panel with data-success="<id>". One validator in part 5 of assets/js/main.js checks them with the browser's own constraints (required, type="email", pattern, min/max, minlength):

Form Pattern Fields
Reservation (reservation, panel #reservation-success) reservation-form date (today to two months ahead, an open day), time, guests (1 to 8, or "9 or more"), seating, name, email, phone (digits, spaces, +, brackets and dashes, at least 7), occasion (optional), allergies or requests (optional, up to 300 characters), newsletter checkbox
Private dining enquiry (enquiry, #enquiry-success) enquire name, email, space (package), type of event, preferred date (today or later), guests (8 to 60), details (at least 20 characters)
Contact (contact, #contact-success) contact-form name, email, topic, message (at least 10 characters)
Newsletter (newsletter, #newsletter-success) footer email

Each field's messages are attributes in the form's HTML: data-msg-required, data-msg-pattern, data-msg-min and data-msg-max (they are translatable); other errors use the script's own messages. Errors appear in the element whose id is the field's id plus -error, fields get aria-invalid, and focus moves to the first invalid one. When everything is valid, the form is hidden and its thank-you panel shown, with focus on its heading; elements with data-fill="name" (or date, time, guests, seating, package, topic) show what was entered (the first name only, the date written out; a word marked data-count-of="guests" switches between its data-one and data-other text, "1 guest" or "2 guests"). The panel's data-reset button brings back an empty form.

Nothing is sent anywhere: they are demo forms until you connect them. When you do, also rewrite the thank-you panels, which say that nothing was sent.

Option 1: a form plugin#

The usual way on WordPress. Install a form plugin (WPForms, Gravity Forms, Fluent Forms, Contact Form 7, Jetpack Forms...), build the form there, then delete the form's Custom HTML block and its thank-you panel (a second Custom HTML block) and insert the plugin's block or shortcode in their place. The plugin sends the messages and stores them. To make its fields look like the theme's, add the theme's classes where the plugin allows custom classes: field on fields, field-label on labels, input on inputs, btn btn-primary on the submit button. A plugin's form won't have the reservation time slots.

Option 2: a form service#

Keep the theme's forms and send them to a service such as Formspree:

  1. Create a form on the service and copy its endpoint URL.

  2. Open the form's Custom HTML block and add the endpoint and method to the form tag, for example on the Reservations page:

    html
    <form id="reservation-form" class="rounded-[2rem] border border-sand bg-cream p-6 shadow-sm sm:p-10" action="https://formspree.io/f/your-id" method="post" data-validate="reservation" data-success="reservation-success" novalidate="">
  3. In assets/js/main.js, in the forms' submit handler (it follows the comment "Demo only: nothing is sent anywhere."), replace the lines from var data = new FormData(form); to the end of the if (success) { ... } block with a version that sends a form with an action, and only shows its thank-you panel when the service accepts it:

    js
    var data = new FormData(form);
    function showSuccess() {
      if (!success) return;
      success.querySelectorAll('[data-fill]').forEach(function (el) {
        var key = el.dataset.fill;
        var value = data.get(key) || '';
        if (key === 'date' && value) value = prettyDate(value);
        if (key === 'name') value = String(value).split(' ')[0];
        el.textContent = value;
      });
      form.hidden = true;
      success.hidden = false;
      var heading = success.querySelector('[tabindex="-1"]');
      if (heading) heading.focus();
    }
    if (!form.getAttribute('action')) { showSuccess(); return; } // forms not connected yet
    fetch(form.action, { method: 'POST', body: data, headers: { Accept: 'application/json' } })
      .then(function (res) { if (!res.ok) throw new Error('Request failed'); showSuccess(); })
      .catch(function () { alert('Sorry, something went wrong. Please call us on +1 (555) 014-2020.'); });

    Make this change in a child theme (see Child themes), or it will be overwritten when you update the theme. Each form can have its own endpoint.

Option 3: a booking system#

For real reservations you will usually want a booking system (OpenTable, Resy, SevenRooms...) rather than email: it knows which tables are free. Replace the reservation form's two Custom HTML blocks with the system's widget (or a button linking to your booking page), and keep the page header and the opening hours sidebar around it. The open/closed badges keep working from HOURS.

SEO and sharing#

Page titles come from WordPress: the page title, an em dash and the site name (for example "Menu — Olive & Ember"); 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.

Restaurants benefit from Restaurant structured data (address, phone, opening hours, menu URL): most SEO plugins and local business plugins can add it.

Accessibility#

Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks (header, navigation with labels, main, footer, labelled sections and aside), one h1 per page, visible gold focus rings, the mobile menu button with aria-expanded and Esc to close, menu tabs with the WAI-ARIA tabs pattern and arrow keys, dietary filter buttons with aria-pressed in a labelled group and the result announced with aria-live, dietary badges with screen-reader text ("Vegetarian"), radio-button seating cards, form errors linked with aria-describedby, aria-invalid on wrong fields, thank-you panels that receive focus, a labelled map with a text description, the "Open now" status as text (not only the coloured dot), keyboard-friendly Details blocks, social links with names, and reduced-motion support for reveals, the flickering glows, smooth scrolling and the back-to-top button.

When you edit: keep one Heading 1 per page, give images alt text (or mark them decorative), keep link text meaningful, keep the screen-reader text on dietary badges, label any field you add to a form (<label for="...">), and check the contrast of any colour you change (WCAG AA).

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, a template or assets/js/main.js, 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.

pnpm start starts WordPress without rebuilding the CSS; pnpm dev does both.

To deploy, zip the theme folder (named freeframe-bistro, 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-bistro. All text in patterns (including the Custom HTML blocks and their data-msg-* messages), templates and PHP goes through WordPress's translation functions. The script's strings (the open/closed status, day names, the reservation time notes, the filter announcement, menu labels, form errors, "Back to top") are translated in PHP (freeframe_bistro_script_strings() in inc/setup.php, printed as window.freeframeBistroL10n). Create a translation with Loco Translate, or with WP-CLI and Poedit:

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

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

These don't follow the site's language: times are written in the 12-hour "7:30pm" style (formatTime() in assets/js/main.js), the date in the reservation thank-you panel uses the visitor's browser language, the seating choices' values ("Dining room", "Counter", "Terrace", shown in the thank-you panel) are English, the map's labels are inside its SVG, and prices are plain numbers you write. See Private dining packages for the Space options.

Child themes#

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

css
/*
Theme Name: Bistro Child
Template: freeframe-bistro
*/

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 opening hours, time zone and any change to the script (sending the forms) belong in a copy of assets/js/main.js in the child theme, loaded in place of the parent's from the child's functions.php. Keeping the script's handle keeps its translations:

add_action( 'wp_enqueue_scripts', function () {
	wp_scripts()->registered['freeframe-bistro']->src = get_stylesheet_directory_uri() . '/assets/js/main.js';
}, 30 );

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 > Bistro.
  • The "Open now" badge is wrong, or an hour or more out. Check HOURS and set TIMEZONE in assets/js/main.js to your restaurant's time zone (see Opening hours).
  • No reservation times appear. That weekday is null in HOURS, or opening time to closing time minus LAST_SEATING_BEFORE_CLOSE leaves no slot.
  • A dish doesn't show up when filtering. Its Group needs the class ffd-dish and the right ffd-diet-- codes (v, vg, gf, joined with _).
  • /menu/#drinks doesn't open the Drinks tab. The panel's HTML anchor must match the tab button's aria-controls.
  • An Enquire button doesn't preselect the space. Its ffd-package-- class must match an option of the Space select exactly, with _ for spaces (see Private dining packages).
  • The header is transparent over a light section, or hides the top of a page. The header is only transparent when the page has an ff-hero section, which must be the dark first section. Start the page with a page-header pattern, or remove ff-hero from a section that isn't the first (see Page headers).
  • The header menu shows other pages, or an old menu. A Navigation block without a chosen menu shows the most recent one. Select the desktop one and choose "Main menu", and the one in the mobile menu and choose "Mobile menu", in the block settings.
  • Bistro 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 > Bistro pages.
  • A form, the map or a portrait is gone after someone saved the page. Forms and SVG in Custom HTML need the unfiltered_html capability, which administrators have on a single site; when other roles (or site admins on multisite) save the page, WordPress strips them. Edit those pages as an administrator, or use a form plugin.
  • The forms "send" but nothing arrives. They are demo forms; see Forms.
  • A page shows the title twice or no title. Pages use a template without a title, because the page patterns start with their own heading. For a plain text page (privacy policy, terms), choose the Page with title template in the page's settings.

License#

MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme folder). Free for personal and commercial use. The dish, chef, fire and wine illustrations, the team portraits, the map and the icons are included under the same license. Cormorant Garamond (assets/fonts/cormorant-garamond/OFL.txt) and Manrope (assets/fonts/manrope/OFL.txt) are licensed under the SIL Open Font License 1.1.

Search documentation

Search the guides and every template's documentation