Overview#
Bistro is a warm, high-end website for a restaurant, café or wine bar. The demo restaurant is Olive & Ember, a fictional wood-fired Mediterranean kitchen in an old town, with tabbed menus, a reservation form whose time slots follow the opening hours, private dining packages and a live "open now" badge. It is built with HTML5, Tailwind CSS v4 and a small vanilla JavaScript file, and needs no build step. This guide covers version 1.1.5.
Live demo: freeframe.dev/templates/bistro
Quick start#
-
Unzip the download and open
index.htmlin a browser. -
Or serve the folder with any static server:
shcd bistro python3 -m http.server 8000 # then open http://localhost:8000
Node.js 24 is only needed to rebuild the CSS (see Building and deploying).
Pages#
| File | Purpose | Notable sections |
|---|---|---|
index.html |
Home | Fire hero, open-now strip, intro and stats, signature dishes, chef quote, reviews, hours and booking call to action |
menu.html |
Menus | Lunch / Dinner / Drinks / Desserts tabs, vegetarian / vegan / gluten-free filter, allergen notes |
about.html |
Story | Timeline, values, team, suppliers |
reservations.html |
Book a table | Reservation form with day-dependent time slots, policies sidebar |
events.html |
Private dining | Three packages, upcoming events, enquiry form |
contact.html |
Visit | Contact cards, opening hours with today highlighted, illustrated map, contact form, FAQ |
404.html |
Not found | Error page with links home, to the menu and to reservations |
Every page has a newsletter form in the footer.
Project structure#
bistro/
├── index.html, menu.html, about.html, reservations.html, events.html, contact.html, 404.html
├── assets/
│ ├── css/style.css compiled Tailwind CSS (generated)
│ ├── js/main.js hours, menu, tabs, filter, forms, reveals, back to top
│ └── img/ SVG food, chef, fire and wine art, favicon, social image
├── src/input.css Tailwind source: design tokens (@theme) and component classes
├── package.json scripts to rebuild the CSS
├── template.json template metadata used by freeframe.dev
├── README.md
├── CHANGELOG.md
└── DOCS.md this guideassets/js/main.js is split into five numbered sections: 1. Opening hours, 2. Header + mobile
menu, 3. Scroll reveal, 4. Tabs + dietary filter and 5. Forms, followed by the back-to-top
block.
Customising#
Brand and colours#
The palette and fonts are tokens in the @theme block at the top of src/input.css:
| Token | Used for |
|---|---|
--color-char, --color-char-2, --color-char-3 |
Dark charcoal backgrounds (header, hero, dark bands) |
--color-cream, --color-parchment, --color-sand |
Light backgrounds and borders |
--color-ember, --color-ember-dark, --color-ember-light |
Primary accent: buttons, links |
--color-olive, --color-olive-light |
Secondary accent |
--color-gold |
Highlights, "Today" tags, dietary notes |
--color-muted |
Secondary text |
For example, a deep blue accent instead of ember:
@theme {
--color-ember: #2b4c7e;
--color-ember-dark: #1f3a63;
--color-ember-light: #7c9ccf;
}Rebuild the CSS afterwards and update the theme-color meta tag in each page's <head>.
Typography#
Headings use Cormorant Garamond (--font-display) and text uses Manrope (--font-sans), loaded
from Google Fonts in each page's <head>. Replace that <link> on every page and the two tokens to
change them.
Content#
Text is plain HTML in the page files; the restaurant name, address and phone number appear on every page, so use search and replace to rebrand.
Opening hours are set once in section 1 of assets/js/main.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 },
// ...
};
var LAST_SEATING_BEFORE_CLOSE = 90; // minutes
var SLOT_STEP = 30; // minutes between reservation slots
var TIMEZONE = 'America/New_York'; // the restaurant's time zone (IANA name)Set TIMEZONE to your restaurant's zone (for example 'Europe/London' or 'America/Chicago'). "Now" is
always read in that zone by businessNow(), so every visitor sees the same open/closed status, today's
row and bookable times, wherever their device is.
HOURS drives the open/closed badge, the highlighted row in the hours tables and the reservation
time slots. The hours printed in the HTML (hours tables on contact.html, the footer and other
pages) are plain text: update them to match. Each hours row lists the weekdays it covers so today's
row can be highlighted:
<div class="hours-row border-cream/15" data-day="2,3,4"><dt>Tuesday – Thursday ...</dt><dd class="text-cream/60">5pm – 10pm</dd></div>Menu dishes on menu.html are list items inside a tab panel and a group:
<div data-menu-group>
<h3>Small plates</h3>
<ul>
<li class="flex gap-6 border-b border-dashed border-sand py-5" data-dish data-diet="v gf">
...dish name, description and price...
</li>
</ul>
</div>data-diet holds space-separated codes: v (vegetarian), vg (vegan) and gf (gluten-free), or is
empty. Show the matching badges (badge-v, badge-vg, badge-gf) inside the dish so guests can see
them. Every dish needs data-dish; every group of dishes needs data-menu-group.
Private dining packages on events.html each have an "Enquire" link with
data-package="The Long Table"; the value must match an <option> in the enquiry form's package
select (see Forms).
Images and artwork#
All artwork in assets/img/ is original SVG: dishes (dish-*.svg), chef.svg, hero-fire.svg,
wine.svg, plus favicon.svg and the social image (og-image.svg source, og-image.png export at
1200×630). To use photos, save them in assets/img/ and change the src attributes; keep similar
proportions (dish images are square-ish, the hero is wide) and write descriptive alt text.
Navigation, header and footer#
The header, mobile menu (#mobile-nav) and footer are repeated in every page. To add a page, copy one,
replace the content inside <main>, and add the link to the desktop navigation and #mobile-nav on
every page, marking the current page with aria-current="page".
Features#
Open now badge and today's hours#
Any element with data-open-status shows the live status from HOURS, for example "Open now · until
10pm" or "Closed · opens tomorrow at 5pm", and gets data-state="open" or "closed" for styling. Its
[data-open-text] child receives the text. Rows with data-day get the is-today class on matching
weekdays. The status refreshes every minute.
The status uses the time in TIMEZONE, not the visitor's device clock, so a guest browsing from another
country still sees whether you're open right now.
Menu tabs#
The tab bar on menu.html (data-tabs) follows the WAI-ARIA tabs pattern: buttons with role="tab",
aria-selected and aria-controls pointing at panels. Left/Right arrows move between tabs and
Home/End jump to the first or last. Clicking a tab updates the URL hash, and opening
menu.html#drinks selects the Drinks tab. To add a tab, add a button and a panel with matching
aria-controls and id.
Dietary filter#
Buttons with data-diet-filter (all, v, vg, gf) show only matching dishes in every tab. Vegan
dishes also count as vegetarian. Groups with no matching dishes are hidden, an empty-state message
([data-empty] inside a panel) appears if a whole tab is empty, and [data-filter-status] announces the
result ("Showing 14 matching dishes and drinks.").
Reservation time slots#
On reservations.html the time select is filled from the chosen date:
- Slots run every
SLOT_STEPminutes from opening untilLAST_SEATING_BEFORE_CLOSEminutes before closing. - For today, only slots at least an hour from now are offered.
- Closed days (
nullinHOURS) show "Closed that day" and the date field explains why. - The date can't be in the past (
data-min-today) or more than two months ahead (data-max-months="2"sets the field'smaxfrom today's date inTIMEZONE). - Choosing "9 or more" guests shows a note pointing to private dining (
[data-party-note]). - A note under the field shows that day's kitchen hours and last seating.
Character counters#
A <textarea> with data-count and a maxlength shows "12 / 300" in the element whose id is the
textarea's id plus -count (for example res-notes-count).
Mobile menu and header#
The header (data-header) gains a background when the page scrolls. Below desktop width the button with
data-nav-toggle opens #mobile-nav, updates aria-expanded, and closes with Esc (returning focus)
or when the window gets wider.
Scroll reveals#
Elements with data-reveal fade in as they enter the viewport; delay them with style="--d:100ms".
Reduced-motion settings make them appear immediately.
Back to top button#
Every page gets a "Back to top" button in the bottom-right corner, created by the "Back to top" block at
the end of assets/js/main.js and styled by the .back-to-top rules at the end of src/input.css. It
appears after about one screen of scrolling, scrolls to the top (instantly with reduced motion), moves
focus to the main content and lifts above toasts. Add data-back-to-top-avoid to anything you pin to
the bottom of the screen, or data-back-to-top-hide to a sticky element it must never cover. To remove
it, delete that JavaScript block and the CSS rules, then rebuild the CSS.
Forms#
Every form is marked data-validate="<kind>" and names its success panel with
data-success="<id>":
| Form | Page | Fields |
|---|---|---|
Reservation (data-validate="reservation") |
reservations.html |
date, time, guests, seating (radio cards), occasion, name, email, phone, notes |
Private dining enquiry (enquiry) |
events.html |
type, package, date, guests (8–60), name, email, message (20+ characters) |
Contact (contact) |
contact.html |
topic, name, email, message (10+ characters) |
Newsletter (newsletter) |
Footer of every page | email |
Validation uses the browser's constraints (required, type="email", pattern, min/max,
minlength) with custom messages from data attributes on each field: data-msg-required,
data-msg-pattern, data-msg-min and data-msg-max. Errors appear in the element with id
<field-id>-error and the first invalid field is focused. On success the form is hidden and its success
panel shown; elements with data-fill="name" (or date, time, guests, seating) are filled with the
submitted values (a word marked data-count-of="guests" switches between its data-one and data-other
text, so the panel says "1 guest" or "2 guests"), and a [data-reset] button brings the form back.
Nothing is sent until you connect the forms. In section 5 of assets/js/main.js, find the comment
"Demo only: nothing is sent anywhere" in the submit handler and post the data first, for example to
Formspree:
fetch('https://formspree.io/f/YOUR_ID', {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' }
}).then(function (res) {
if (!res.ok) throw new Error('Request failed');
// ...existing success code: fill data-fill elements, hide the form, show the success panel
}).catch(function () {
alert('Sorry, your request could not be sent. Please call us.');
});Use form.dataset.validate to send each form type to a different endpoint. Netlify Forms also works:
add data-netlify="true" and a name to each <form>. For real reservations you will usually want a
booking system (OpenTable, Resy, SevenRooms...) instead of email; replace the reservation form with their
widget or link, and keep the page around it.
SEO and sharing#
Each page has its own <title>, meta description, Open Graph tags and twitter:card. When you deploy,
change og:image on every page to an absolute URL:
<meta property="og:image" content="https://example.com/assets/img/og-image.png">Restaurants benefit from Restaurant structured data: consider adding a
JSON-LD block with your address, phone, hours and menu URL to index.html and contact.html, and a
sitemap.xml listing your pages.
Accessibility#
- Skip link, labelled landmarks and one
<h1>per page. - Menu tabs use the tabs pattern with arrow-key support; filter buttons use
aria-pressed. - Dietary badges have visible codes plus screen-reader text ("Vegetarian").
- Form errors are linked with
aria-describedby, fields getaria-invalid, and success panels receive focus. - Animations (reveals, the flicker on the fire art) respect
prefers-reduced-motion.
When editing, keep the screen-reader text on new dietary badges, label new form fields, and keep one
<h1> per page.
Building and deploying#
assets/css/style.css is pre-built. Rebuild it after editing src/input.css or using Tailwind classes
that aren't in the CSS yet. With Node.js 24:
npm install
npm run build # or: npm run watchBistro is a static site: upload the folder (without node_modules/) to Netlify, GitHub Pages,
Cloudflare Pages or any web host. No build command is needed on the host.
Troubleshooting#
- The open/closed badge is an hour or more out. Set
TIMEZONEat the top ofassets/js/main.jsto your restaurant's IANA zone name (for example'Europe/London'). - No reservation times appear. Check
HOURSfor that weekday isn'tnull, and that opening hours minusLAST_SEATING_BEFORE_CLOSEleaves at least one slot. - A dish doesn't show up when filtering. It needs
data-dishand the right codes indata-diet(v,vg,gf). menu.html#drinksdoesn't open the Drinks tab. The hash must match the tab'saria-controlsvalue.- The "Enquire" button doesn't preselect a package. Its
data-packagemust exactly match an<option>in thepackageselect. - New Tailwind classes have no effect. Rebuild the CSS.
- Forms "send" but nothing arrives. Connect them as described in Forms.
License#
Bistro is released under the MIT License (see LICENSE.txt in the download), for personal and
commercial use. The included SVG artwork is covered by the same license. Cormorant Garamond and Manrope
are licensed under the SIL Open Font License.