Overview#
Market is a complete storefront for Kiln & Co., a fictional handmade-ceramics studio: home, a filterable shop, product pages with glaze and size variants, a cart saved in the browser, a slide-out mini cart, a validated demo checkout, an about page and a searchable FAQ with a contact form. Product images are drawn in SVG from the product data, so there are no photos to replace before launch. This is the HTML + Tailwind CSS v4 version, version 1.1.4, with vanilla JavaScript and no runtime dependencies. See it live at freeframe.dev/templates/market.
Quick start#
Open index.html in a browser, or serve the folder (recommended, so links with query strings behave like they
will in production):
npx serve .To change styles you need Node.js 24 or newer:
npm install
npm run watch # rebuilds assets/css/style.css while you edit
npm run build # one-off minified build| Command | What it does |
|---|---|
npm run build |
Compile src/input.css to assets/css/style.css (minified) |
npm run watch |
Same, rebuilding on every change |
Pages#
| File | data-page |
What's on it |
|---|---|---|
index.html |
home |
Hero with product art, shop by shape, bestsellers (rendered from featured products), story, reasons to buy, reviews, newsletter band |
shop.html |
shop |
Product grid with category, price and sort filters and a live result count |
product.html |
product |
One product, chosen by ?id=: gallery with four views, glaze and size options, quantity, details, related products |
cart.html |
cart |
Cart lines with quantity steppers, free-shipping progress, promo code, order summary |
checkout.html |
checkout |
Contact, shipping address, delivery method and card fields, order summary, confirmation |
about.html |
about |
Studio story, timeline, team and visit info |
faq.html |
faq |
Searchable FAQ grouped by topic, and a contact form (#contact) |
404.html |
404 |
Not-found page |
main.js reads <body data-page="…"> to decide which page script to run, so keep that attribute when you copy
a page.
Project structure#
market/
├─ index.html, shop.html, product.html, cart.html, checkout.html, about.html, faq.html, 404.html
├─ assets/
│ ├─ css/style.css compiled Tailwind output (don't edit; edit src/input.css)
│ ├─ js/main.js catalogue, SVG art, cart, page scripts, forms, back to top
│ └─ img/ favicon.svg, og-image.png (1200×630), og-image.svg (its source)
├─ src/input.css Tailwind entry: brand tokens, components, animations
├─ package.json Tailwind CLI build scripts
└─ README.md, DOCS.md, CHANGELOG.md, template.jsonassets/js/main.js is split into numbered sections: 1 Product catalogue, 2 SVG product art, 3 Helpers,
4 Cart, 5 Cart drawer and toast, 6 Header and mobile menu, 7 Home, 8 Shop, 9 Product page, 10 Cart page,
11 Checkout, 12 Forms and FAQ, then the back-to-top script.
Customising#
Brand and colours#
All colours are tokens in the @theme block of src/input.css:
| Token | Default | Used for |
|---|---|---|
--color-paper |
#f6f1ea |
Page background |
--color-sand / --color-oat |
#ebe3d6 / #e2d7c6 |
Panels and image tiles |
--color-line |
#d9cebd |
Borders |
--color-ink |
#1f1c19 |
Text and primary buttons |
--color-stone |
#645d54 |
Muted text |
--color-clay / --color-clay-dark |
#a1441d / #7f3414 |
Accent, links, focus rings, badges |
--color-moss |
#56653f |
Secondary accent (shipping progress bar) |
--color-cream |
#fbf8f3 |
Light surfaces and form fields |
Change them and run npm run build. Also update <meta name="theme-color"> in each page and the colours inside
assets/img/favicon.svg. Button styles are .btn-primary, .btn-accent, .btn-outline and .btn-light.
Typography#
Fraunces (display, font-display) and Inter (body, font-sans) load from Google Fonts in each page's
<head>. To change them, edit that <link> in every page and the --font-display / --font-sans tokens.
Content#
Products are the PRODUCTS array at the top of assets/js/main.js. Each product looks like this:
{
id: 'ridge-vase', name: 'Ridge Vase', category: 'Vases', price: 68, shape: 'vase',
rating: 4.9, reviews: 214, badge: 'Bestseller', featured: 1,
blurb: 'A tall, generous vase thrown in one pull…',
glazes: [{ name: 'Oat', color: '#e6d9c2' }, { name: 'Moss', color: '#76855a' }],
sizes: [{ name: 'Medium · 24cm', delta: 0 }, { name: 'Large · 32cm', delta: 26 }],
details: ['Stoneware, fired to 1260°C', 'Fully glazed inside — watertight'],
dims: 'Medium: 24cm tall, 14cm wide. Large: 32cm tall, 18cm wide.'
}| Field | Notes |
|---|---|
id |
Unique, used in product.html?id=… and in saved carts. Changing an id drops it from visitors' carts |
category |
Must be one of CATEGORIES; add new categories there and to the radio list in shop.html |
price |
Base price in dollars; each size adds its delta (which can be negative) |
shape |
vase, bud, mug, cup, tumbler, bowl, plate, pitcher or planter (drives the SVG art) |
badge |
Optional label such as Bestseller, New or Limited |
featured |
Optional; products with it appear in the home page bestsellers, sorted by the number |
glazes |
Colour options; color is the glaze colour in the art |
sizes |
Size options; the one with delta: 0 is preselected |
Store settings sit just below the array:
| Constant | Default | Meaning |
|---|---|---|
FREE_SHIPPING |
75 |
Subtotal for free standard shipping |
STANDARD_SHIPPING / EXPRESS_SHIPPING |
8 / 18 |
Delivery prices |
TAX_RATE |
0.08 |
Tax applied after the discount |
PROMO |
{ code: 'WELCOME10', rate: 0.10 } |
The demo promo code |
MAX_QTY |
99 |
Largest quantity per cart line |
Prices are formatted with Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }) (the money
helper); change the locale and currency there. The "Free shipping on orders over $75" announcement bar and the
shipping FAQ are plain text in the HTML, so update them when you change FREE_SHIPPING.
Images and artwork#
Every product image is generated by art(product, glazeIndex, view) from the product's shape and glaze
colour. Views are 0 front, 1 glaze close-up, 2 on a shelf and 3 a pair. Place art anywhere with:
<div class="art aspect-square overflow-hidden rounded-[1.75rem]" data-art="terrace-planter" data-glaze="1" data-view="2"></div>To use photos instead, add an image field to each product and return an <img> from art() (or change
productCard() and the product gallery). The social image is assets/img/og-image.png (1200×630); its source
is og-image.svg.
Navigation, header and footer#
The header, mobile menu, footer, cart drawer (#cart-drawer, #cart-overlay) and toast (#toast) are
repeated in every page, so edit them in all eight files. Mark the current page's links with
aria-current="page". Category links use the shop query string, for example
shop.html?category=Mugs%20%26%20Cups. The footer social links point to #; replace them with your profiles.
Features#
Cart and mini cart#
The cart is saved in localStorage and synced across tabs. Any element with data-cart-open opens the
slide-out cart; product cards add with data-add="<product id>". The drawer traps focus, closes with Escape,
the close button or the backdrop, and returns focus to where it was. Adding an item bumps the header badge
(data-cart-count) and shows a toast with a "View cart" button.
Each cart line (in the drawer and on cart.html) carries data-line="<id>:<glaze>:<size>". After −, + or
Remove, keyboard focus stays on the same button of the same line, and the line is updated in place so the
quantity's live region reads out the new number. When a line goes (Remove, or − at quantity 1), focus moves to
the same button on the next line, or on the previous line if it was the last; when the cart is then empty, it
moves to the empty cart's "Shop" link ("Shop the collection" in the drawer, "Shop all ceramics" on the cart
page).
Shop filters#
shop.html filters by category (radio buttons, counts from data-count-for), maximum price (#price-max) and
sort (#sort: featured, price up/down, rating, name). The result count and heading update live, and the
category is kept in the URL (?category=Vases) so filtered views can be linked. On small screens the filters
fold behind #filters-toggle.
Product page#
product.html?id=daily-mug shows that product (an unknown id shows the first product). The page title,
breadcrumb and text fill from the product. Glaze swatches recolour the gallery; sizes change the price;
the quantity stepper is limited to 1–99; four thumbnails switch views. Related products prefer the same
category.
Promo code and shipping progress#
On the cart page, WELCOME10 takes 10% off. The free-shipping progress bar appears in the drawer and cart page.
Checkout#
The checkout validates every field, formats the card number and expiry as you type, checks the card number
with the Luhn algorithm (use 4242 4242 4242 4242) and rejects expired dates. On success it shows a
confirmation with a random KC- order number and clears the cart. No payment is processed. Replace the
setupValidation(form, …) callback in initCheckout() with your payment provider; the comment there marks
the spot. For real payments, use a hosted checkout such as Stripe Checkout or Shopify, and don't collect card
numbers in your own form.
FAQ search#
Typing in #faq-search filters the details[data-faq] items, opens matches, hides empty [data-faq-group]
sections and announces the number of answers.
Back to top button#
A button appears after scrolling about one screen, returns to the top (instantly with reduced motion), moves
focus to #main, lifts above the toast and hides while an in-page "Back to top" link is visible. Add
data-back-to-top-avoid (lift above) or data-back-to-top-hide (hide while covering) to your own fixed UI. To
remove it, delete the "Back to top" block at the end of main.js and the .back-to-top styles in
src/input.css.
Forms#
All forms are validated by setupValidation() in main.js: errors appear on blur and submit, the first
invalid field is focused, and each error is linked with aria-describedby. Rules come from attributes:
| Attribute | Rule |
|---|---|
required |
Must not be empty |
type="email" |
Must look like an email address |
minlength |
Minimum length |
data-label |
Name used in the error message |
data-validate |
card, expiry, cvc or postcode |
| Form | Where | Behaviour |
|---|---|---|
Newsletter (data-form="newsletter") |
Every footer, home page band | Replaced with a thank-you message |
Contact (data-form="contact") |
faq.html#contact |
Replaced with a thank-you message |
Promo (#promo-form) |
cart.html |
Applies WELCOME10 |
Checkout (#checkout-form) |
checkout.html |
Shows the order confirmation |
Nothing is sent. To collect submissions, post the form in its onValid callback, for example to
Formspree:
$$('[data-form="contact"]').forEach(function (form) {
setupValidation(form, function () {
fetch('https://formspree.io/f/your-id', {
method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' }
}).then(function (res) {
successSwap(form, res.ok ? '<p>Thank you for writing.</p>' : '<p>Something went wrong. Please email us.</p>');
});
});
});FormData only includes fields with a name. Every field already has one (email, name, topic, order,
message, and the checkout fields), so the request carries all the values; keep a name on any field you add.
Saved data (localStorage)#
| Key | Value | Used for |
|---|---|---|
kiln-cart |
JSON array of { id, glaze, size, qty } |
Cart contents |
kiln-promo |
"WELCOME10" or null |
Applied promo code |
The cart is validated every time it is read: unknown products and bad entries are dropped, glaze and size
indexes are range-checked, quantities are clamped to 1–MAX_QTY, duplicate lines are merged, and tampered
data is overwritten with the cleaned version. Prices are never stored; totals are always calculated from
PRODUCTS. If storage is blocked (some private windows), the store still works, but the cart is empty after
a reload.
SEO and sharing#
- Each page has its own
<title>and<meta name="description">. The product page also setsdocument.titlefrom the product. - Open Graph and Twitter tags are in each
<head>.og:imageis the relative pathassets/img/og-image.png; most social networks need an absolute URL, so change it tohttps://your-domain/assets/img/og-image.pngand add<meta property="og:url">and<link rel="canonical">when you deploy. - Product pages are filled in by JavaScript from
?id=, so search engines that don't run scripts see the default content. If product search traffic matters, create one HTML file per product with its text written into the page. - Add a
sitemap.xmlandrobots.txtat the root for production.
Accessibility#
Built in: a skip link, landmarks, visible focus rings, a focus-trapped cart drawer that closes with Escape,
a mobile menu with aria-expanded that closes with Escape, a live cart count in the cart button's label, polite
toasts, aria-pressed gallery thumbnails, glaze names for screen readers, an ARIA progress bar for free
shipping, inline form errors linked to fields, a live FAQ result count, reduced-motion support and WCAG AA
contrast. Content still shows without JavaScript except the parts drawn from the catalogue (art, grids and the
cart).
When editing, keep one <h1> per page, give each glaze a meaningful name (it's the image's text alternative)
and keep the data-label attributes on form fields.
Building and deploying#
- Run
npm run buildif you changedsrc/input.cssor added Tailwind classes to HTML or JS. - Upload the folder (without
node_modules/,src/andpackage.jsonif you like) to any static host: Netlify, Vercel, Cloudflare Pages, GitHub Pages or plain hosting. - Configure the host to serve
404.htmlfor missing pages.
Tailwind scans the HTML and assets/js/main.js for class names, so classes used only inside JavaScript strings
are included as long as they appear in full.
Troubleshooting#
- A new class has no effect. Run
npm run build, and write class names in full (not built from pieces). - A product shows the Ridge Vase instead. The
?id=doesn't match any productid. - A category filter shows nothing. The product's
categorymust match a value inCATEGORIESand the radio'svalueexactly. - The cart forgets items.
localStorageis blocked, or the product'sidchanged. - Social previews have no image. Use an absolute
og:imageURL. - Totals look wrong after changing prices. Totals always use current
PRODUCTSprices; carts don't store prices, so this is expected.
License#
MIT. The code and artwork are free for personal and commercial use; the download includes the license text in
LICENSE.txt. Fraunces and Inter are licensed under the SIL Open Font License.