Freeframe

Documentation

Frond

v1.0.0Updated Oct 19, 2026A bright, characterful houseplant shop with a plant matcher and a proper basket.

Overview#

Frond is a complete HTML and Tailwind CSS v4 website for an independent houseplant shop on Gracious Street in Knaresborough, UK. Version 1.0.0 has nine pages and a 404: a catalogue of fifteen illustrated plants with sizes and cover pots, a saved basket, a three-step checkout with gift options, a card step and an order confirmation, a four-question plant finder, per-plant care cards and a visit page with a map and live opening hours. The business and people are fictional. See the template page for the live demo and download.

Quick start#

  1. Unzip the download.
  2. Open index.html in your browser, or serve the folder (recommended, so saved baskets behave as on a real site):
sh
python3 -m http.server 8080
  1. Visit http://localhost:8080/index.html.

Any static server works. Node 24 is needed only to rebuild the Tailwind CSS. The site has no JavaScript dependencies, API keys or database. Fonts load from Google Fonts, so they need a network connection.

Pages#

File Purpose Notable sections
index.html Home Plant-room hero, four featured plants, finder promotion, three-step “Frond way”, visit panel
shop.html Catalogue Fifteen plants, search, four filters, sorting, quick add
product.html Plant details Query-driven plant, two sizes, optional cover pot, dispatch date, care, size and delivery tabs
basket.html Basket Variant lines, quantities, delivery or collection, postcode check, welcome code, last order
checkout.html Checkout Details, delivery or collection with gift message and wrap, card details, order confirmation
finder.html Plant finder Four questions, ranked matches with a grade and a reason per plant, shareable URL
care.html Care cupboard Per-plant care cards, five symptom guides, weekly, monthly and seasonal habits
about.html Our roots Illustrated shopfront, shop history, three illustrated staff portraits, packaging
visit.html Visit and contact Address, illustrated map, today’s hours and the next bank holiday, access notes, contact form
404.html Missing page Routes back to the shop, home and contact

product.html?plant=monstera opens the monstera directly. A missing or unknown plant parameter falls back to the first plant in the catalogue, with a short notice when an unknown one was asked for.

Project structure#

frond/
  index.html, shop.html, product.html, basket.html, checkout.html,
  finder.html, care.html, about.html, visit.html, 404.html
  assets/
    css/style.css            Compiled Tailwind output
    js/data.js               FROND_PRODUCTS, the catalogue and per-plant care text
    js/main.js               Dates, basket, checkout, finder, care, forms and storage
    img/                     Fifteen plant drawings, hero and page art, portraits, favicon, social image
  src/input.css              Tailwind import, brand tokens, small component layer
  package.json               CSS build and watch scripts
  README.md                  Short introduction
  DOCS.md                    This guide
  CHANGELOG.md               Template release history

template.json is gallery metadata and is not part of the download. LICENSE.txt is added to the download.

Customising#

Brand and colours#

Brand tokens live in @theme in src/input.css. Layout and styling are Tailwind utilities in the HTML and in the markup generated by main.js; the small @layer components holds repeated controls (.btn, .field, .choice, .tab, .pill, .card, .eyebrow, .display).

Token Default Use
--color-teal #104b46 Primary brand, buttons and footer
--color-deep #0a3633 Hover and notifications
--color-acid #d8ed75 Bright callouts and badges
--color-orange #f58243 Decorative accents
--color-ink #173d38 Body and heading text
--color-cloud #f1f5ed Quiet panel backgrounds
--color-muted #52665e Supporting text
--color-line #d5dfd4 Borders and dividers
--color-rust #a3331c Inline validation errors
css
@theme {
  --color-teal: #164f50;
}

Rebuild the CSS afterwards. SVG fills are self-contained, so recolour the artwork separately. Check contrast when changing text, focus, button or panel colours.

Typography#

Bree Serif (headings, prices) and Cabin (body text and controls) load from Google Fonts in the <head> of every page:

html
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Bree+Serif&amp;family=Cabin:wght@400..700&amp;display=swap"
/>

--font-display and --font-sans in src/input.css select the faces. To swap fonts, change the link on every page and these two tokens, then rebuild. Re-render the social image with the new fonts (see below).

Content#

Business copy lives in the HTML files. The catalogue is window.FROND_PRODUCTS in assets/js/data.js; assets/js/main.js builds product cards, the product page, basket rows, checkout lines, finder results and care cards from it.

Field Purpose
id Unique URL and basket identifier; also the image file name assets/img/<id>.svg
name Common name shown first (“Spider plant”)
botanical, cultivar Botanical name (shown in italics) and optional cultivar (shown in quotes)
aka Optional other name, included in search (“devil’s ivy”)
price, largeDelta Standard price and the extra for the large size, in whole pounds
light bright, soft or low
effort easy or regular
room Desk, Shelf or Statement (used by the finder and search)
pets, petNote true only if commonly listed as non-toxic to cats and dogs; the wording shown
blurb Short description for the product page
lightNote, water, feed, extra The plant’s own care card
strength The sentence the finder uses as its reason for suggesting the plant
diameter, height Standard growing-pot diameter and height, in centimetres
stock, largeStock Standard and large stock counts (0 disables the large size)
pot, glaze, shape Paired cover pot shape and glaze; shape names the drawing’s silhouette

To add a plant, copy a complete object, give it a new id, write its care text and add assets/img/<id>.svg. Don’t reuse an id for a different plant: saved baskets refer to it. Check toxicity guidance (for example the ASPCA list) before setting pets: true, and keep petNote careful.

js
{
  id: "spider-plant",
  name: "Spider plant",
  botanical: "Chlorophytum comosum",
  cultivar: "Vittatum",
  price: 16,
  light: "soft",
  pets: true,
  effort: "easy",
  room: "Shelf",
}

The excerpt is not complete; keep every field from an existing object. Staff names (Orlaith Sedgewick, Benedicta Threlfall, Euan Brackenridge) are in about.html. The address, phone, hours and email are repeated in the footer of every page and in SHOP_ADDRESS in main.js.

Images and artwork#

File Size Used on
<plant id>.svg (15 files) 400×420 Cards, product page, basket, checkout, care
green-room.svg 720×700 Home hero
shopfront.svg 720×560 About hero
light-guide.svg 720×460 Finder hero
portrait-orlaith.svg, portrait-benedicta.svg, portrait-euan.svg 400×420 About team cards
og-image.svg / og-image.png 1200×630 Social sharing
favicon.svg 64×64 Browser tab

The visit-page map is inline SVG in visit.html, so its labels use the page font; edit the <text> elements and the <title> together. The other SVGs contain no text. The artwork is included with the template.

To use photographs, keep the 400×420 crop for plants (or change the width/height attributes in productCard, setupProduct, setupBasket, setupCheckout and setupCare). Plant images show the paired cover pot; the alt text and the “Cover pot shown is an optional extra” label say so.

Regenerate the social image after editing og-image.svg (the page is passed so Bree Serif and Cabin are used):

sh
node scripts/og-png.mjs templates/html/frond/assets/img/og-image.svg templates/html/frond/assets/img/og-image.png templates/html/frond/index.html

That script is part of the Freeframe repository; any SVG-to-PNG tool works if you have the fonts installed.

The header and footer are repeated in all ten HTML files; edit every copy. Desktop links use .nav-link; mobile links are in #mobile-menu. Mark the current page with aria-current="page" in both. The basket link’s count and label are updated by updateBadges. There is no dark mode.

Features#

Dates, dispatch and opening hours#

Everything dated is worked out in the browser in Europe/London time (TIME_ZONE), so the demo never goes stale.

Constant Default Meaning
DISPATCH.cutoff "12:00" Orders before this time on a dispatch day leave that day
DISPATCH.days [1, 2, 3, 4] Dispatch weekdays (Monday to Thursday; 0 is Sunday)
DISPATCH.transit "1–2 working days" Courier time shown after dispatch
HOURS Tue–Sat 10:00–17:00, Sun 11:00–15:00 Opening hours by weekday; null is closed
BANK_HOLIDAY_HOURS ["11:00", "15:00"] Hours on bank holidays
SHOP_CLOSED ["12-25", "12-26", "01-01"] MM-DD days the shop is shut; add one-off closures here

bankHolidays(year) computes England and Wales bank holidays for any year from the rules (New Year’s Day, Good Friday and Easter Monday from the Easter computus, the two May Mondays, the August Monday, Christmas and Boxing Day with substitute days). They are never dispatch days.

  • dispatchDate(): today if it is a dispatch day before the cut-off, otherwise the next dispatch day. Shown on product pages, the basket, the checkout and the confirmation ([data-dispatch-line]).
  • collectionDate(): collection orders are ready the next day the shop is open ([data-collect-date]).
  • setupVisit(): “Open now, until 5pm”, “Closed today. We open again…” and the next bank holiday the shop opens.

For review, add ?now=YYYY-MM-DDTHH:MM (London time) to any page, for example product.html?plant=monstera&now=2026-12-24T13:00. Invalid values are ignored. For a real shop, set the constants to your own rules; nothing needs turning off.

Catalogue filters#

setupShop renders the catalogue. Search (up to 80 characters, case-insensitive) checks the common name, botanical name and cultivar, aka, blurb and room. Filters combine; price limits are strict (Under £25 excludes a £25 plant). Selections update the URL without reloading; invalid values are ignored. Quick add adds one standard plant in its growing pot.

Query parameter Accepted values
q Search text
light all, bright, soft, low
care all, easy, regular
pets all, safe
price all, 25, 35
sort featured, price-asc, price-desc, name

Product options and tabs#

setupProduct reads plant, size (standard, large) and pot (growing, ceramic). A size with no stock is disabled and can’t be chosen through the URL. coverPrice adds £8 (standard) or £12 (large) for the cover pot; stock is per plant and size, shared across pot choices. setupTabs implements the three tabs with Left/Right, Home and End.

Basket and delivery#

A basket line is a variant id:size:pot; adding the same variant raises its quantity. Prices are always recalculated from the catalogue.

Constant Value Meaning
FREE_DELIVERY 6500 Free mainland delivery threshold in pence, after discounts
DELIVERY_PRICE 595 Mainland delivery in pence
GIFT_WRAP_PRICE 350 Gift wrapping at checkout, in pence
COUPON GROW10 10% off plants and cover pots

The delivery option label shows “free” once the basket qualifies. postcodeResult checks the postcode format and refuses addresses the 48-hour courier can’t reach: the areas in OFFSHORE_AREAS (BT, GY, JE, IM, HS, ZE) and the districts in OFFSHORE_DISTRICTS (Orkney KW15–17, Scilly TR21–25, Bute PA20, Gigha, Islay, Jura, Colonsay, Mull, Iona, Tiree and Coll PA41–49 and PA60–78, Arran and Cumbrae KA27–28, Skye IV41–49, IV51 and IV55–56, the Small Isles PH42–44). Edit those lists to match your courier.

Checkout and order confirmation#

setupCheckout has three steps, each validated before moving on:

  1. Your details: name, email, optional phone.
  2. Getting home: tracked delivery (address, town, postcode) or free collection, an optional gift (card message up to GIFT_MESSAGE_MAX characters, optional gift wrap) and a note.
  3. Check and pay: a review, then card name, number (Luhn check, grouped in fours as you type), expiry (MM / YY, not in the past) and security code. The page says “No payment is taken on this site.”

Placing the order shows a confirmation with an order number (FR- and five digits), the dispatch or collection date, the lines and totals; it empties the basket and saves a short summary as the last order, shown on the empty basket and checkout pages. Personal details and card fields are never stored, and the card inputs have no name attributes.

To take real orders, replace the card fields with your payment provider’s hosted fields (Stripe, Square, PayPal and so on) and, at the comment in the submit handler, send the order to your server, recheck stock, prices and delivery there, take payment, and only then show the confirmation. Never trust prices, stock or discounts calculated in the browser. Connect a stock system so stock and largeStock come from your inventory, and an address lookup if your courier offers one.

Plant finder#

setupFinder asks about light, watering style, pets and where the plant will live (desk, shelf, statement, any). finderScore gives each plant up to 9 points: light 4 for an exact match or 2 for one step away (plants two steps away are left out), care 3 for a match (2 when a regular-care home gets an easy plant, 0 the other way round), space 2 for a match or 1 for “anywhere”. A pet-friendly request is a hard rule. The top six are shown, graded “Best match” (8+), “Good match” (6+) or “Worth a try”, each with a reason from finderReason built from the plant’s own strength text and how it differs from the answers.

Answers are shared through the URL (?light=low&care=easy&pets=safe&space=desk); opening such a link shows the results straight away. Invalid values are ignored.

Care cupboard#

setupCare shows the selected plant’s lightNote, water, feed, extra and petNote (?plant=<id> preselects one). SYMPTOMS holds five symptom guides with title, copy and action, shown in a polite live region. This is general growing guidance, not a diagnosis.

setupHeader toggles the small-screen menu with aria-expanded; Escape closes it and returns focus. setupReveals fades sections in with IntersectionObserver, revealing everything above a section too; reduced-motion users see all content immediately.

Forms#

Form Validation Result
#contact-form Name 2+ characters, valid email, message 20+ characters “Thanks, Name. We’ll reply within a working day.”
#checkout-form Details, address and deliverable postcode, gift message length, card fields Order confirmation
#product-options Known size and pot, quantity clamped to stock A basket line
#basket-options Known route, postcode check, recognised code Saved choices and totals
#plant-finder Answers checked against FINDER_ALLOWED Ranked plants
#shop-filters Known values, capped search Filtered catalogue

Errors appear under their fields, linked with aria-describedby, and the first invalid field gets focus. The contact form doesn’t send anything until you connect it. To use Formspree, create a form and replace showThanks() in the submit handler of setupContact (the comment above the function shows the same code):

js
fetch("https://formspree.io/f/your-form-id", {
  method: "POST",
  body: new FormData(form),
  headers: { Accept: "application/json" },
}).then((response) => (response.ok ? showThanks() : showSendError()));

Write a showSendError() that shows an inline message, and only thank the visitor after a successful response. Add a privacy notice suitable for your service.

Saved data (localStorage)#

All access goes through safeStorage in main.js, which falls back to memory when storage throws (sandboxed previews, strict privacy settings). Then the basket works within the page view but doesn’t survive navigation.

Key Shape Validation on read
frond-basket-v1 Array of { id, size, pot, qty } Array, at most 60 rows, own-key ids and enums, positive quantities clamped to stock, duplicates merged
frond-options-v1 { shipping, postcode, coupon } Known route, postcode letters and digits up to 10 characters, exact code
frond-last-order-v1 { number, total, items, when } FR- plus five digits, integer pence and item count, when capped at 120 characters

Malformed values are discarded. No names, emails, phone numbers, addresses, messages or card details are stored. To reset, run in the browser console:

js
["frond-basket-v1", "frond-options-v1", "frond-last-order-v1"].forEach((key) => localStorage.removeItem(key));
location.reload();

SEO and sharing#

Each page has its own title, description, Open Graph tags, theme-color and locale. The product page title is set in the browser to the plant’s name. Set og:image on every page to an absolute URL such as https://your-site.example/assets/img/og-image.png. No structured data, sitemap or robots file is included; add LocalBusiness and Product data once you have real details, and a sitemap of your final URLs.

Accessibility#

Skip link, landmarks, one h1 per page, visible labels with linked inline errors, a roving tab index on the product tabs, polite announcements for basket changes and results, and managed focus after removals, checkout steps and form success. The map has a text alternative in its <title>. Keep contrast, focus outlines and target sizes when customising, label new inputs, keep each aria-* reference pointing at an existing id, and keep the finder’s pet-safety rule.

Building and deploying#

The compiled CSS is included. After adding utilities or changing tokens, with Node 24:

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

This compiles src/input.css to assets/css/style.css. Upload the whole folder to Netlify Drop, GitHub Pages, Cloudflare Pages or any static host, keeping assets/ beside the HTML. Configure your host to serve 404.html for missing pages. Links are relative, so a subdirectory works too.

Troubleshooting#

New utility classes have no effect#

The CSS is compiled. Run npm run build after adding classes or changing @theme.

The basket empties on another page#

Storage is blocked (private browsing, a sandboxed preview or some file:// setups), so the basket lives in memory for one page view. Serve the folder over HTTP.

The dispatch date looks wrong#

Dates use Europe/London time and the rules in DISPATCH and bankHolidays. Try ?now= to see a given moment, and add one-off closures to SHOP_CLOSED.

A welcome code adds a delivery charge#

Free delivery applies after discounts: £70 less 10% is £63, so delivery is £5.95.

A postcode is refused#

It is outside mainland Great Britain or in one of the island districts listed above. Edit OFFSHORE_AREAS and OFFSHORE_DISTRICTS to match your courier.

Fonts look different offline#

Bree Serif and Cabin come from Google Fonts; without a connection the system fallback fonts are used.

License#

MIT. See LICENSE.txt in the download. All artwork is included with the template.

Bree Serif and Cabin are licensed under the SIL Open Font License.

Frond, its staff and its contact details are fictional.

Search documentation

Search the guides and every template's documentation