{"slug":"kiln","name":"Kiln","version":"2.7.3","category":"e-commerce","priceCents":3400,"description":"A Tailwind ecommerce Next.js template for makers selling small-batch physical goods, built so photography carries the layout. Its full-bleed shelf frames six square product photographs on white bays across the shop, collection and product routes. The collection browser filters the shared catalogue by availability and exact price bands, sorts by shelf or price, announces result-count changes, and recovers from an empty result without inventing a backend. The cart also works: add the Ridge tumbler in the selected glaze, watch the header count change, edit quantities, remove lines, and see subtotal, delivery and total follow from one store. Seven pages include the shop front, collection, Ridge detail, making, delivery, cart and living styleguide. Sixteen sections, twenty-one primitives, twenty-one semantic tokens, three self-hosted OFL typefaces and thirteen AI-generated ceramics photographs ship in the same license. Eleven of the primitives are the repository's own component library, vendored unedited and refined here, so Escape, the focus trap, the focus return and the scroll lock behave the same in every theme that ships them. Built for Tailwind v4, Next.js 15 and React 19; nine client islands, everything else a server component. The seven routes ship a second time as a framework-free HTML template: compiled documents, one stylesheet, the same ceramics photography and vanilla JavaScript, with no build step to run.","summary":"For makers selling small batches: a Tailwind ecommerce Nextjs template with a shop built around photography, collection filters and a working cart. 7 pages.","tags":["tailwind ecommerce nextjs template","ecommerce template for makers","shop","cart","tailwind-v4"],"palette":{"bg":"#f6f1ea","surface":"#ffffff","accent":"#b34a2c","text":"#231c16","line":"#e3d8c8"},"darkMode":true,"pages":[{"path":"/","title":"Shop","sections":["site-header","hero","product-grid","collection-split","promises","reviews","newsletter","site-footer"]},{"path":"/collection","title":"Collection","sections":["site-header","page-header","collection-browser","site-footer"]},{"path":"/product","title":"Ridge tumbler","sections":["site-header","product-detail","reviews","product-grid","site-footer"]},{"path":"/making","title":"The making","sections":["site-header","page-header","collection-split","firing-calendar","product-grid","newsletter","site-footer"]},{"path":"/delivery","title":"Delivery","sections":["site-header","page-header","delivery-rates","promises","returns-faq","small-print","site-footer"]},{"path":"/cart","title":"Cart","sections":["site-header","page-header","cart-contents","site-footer"]},{"path":"/components","title":"The parts","sections":["site-header","hero","product-grid","collection-browser","product-detail","cart-contents","collection-split","firing-calendar","promises","delivery-rates","returns-faq","small-print","reviews","newsletter","page-header","site-footer"]}],"components":[{"name":"site-header","file":"src/components/site-header.tsx","renders":"Sticky bar with the wordmark, three nav links, the light/dark toggle, the live cart drawer and a native mobile menu - a server component composing two client islands"},{"name":"hero","file":"src/components/hero.tsx","renders":"A8 asymmetric offset: the studio statement on a plaster ground - a generated grayscale wall texture blended into the background token - offset in the left three fifths, with the studio photograph taking the right two fifths and bleeding off the page edge"},{"name":"product-grid","file":"src/components/product-grid.tsx","renders":"The shelf: a full-bleed hairline plate of white bays holding square product photographs with name, price and stock state, filterable by id and reusable as a related strip"},{"name":"collection-browser","file":"src/components/collection-browser.tsx","renders":"A local collection browser with availability and price filters, shelf and price sorting, a polite result count, removable active filters, full plates for larger sets, normal-width sparse results and an explicit resettable empty state"},{"name":"product-detail","file":"src/components/product-detail.tsx","renders":"Three-shot gallery of one object at wide width with the lead shot breaking the container, price, rating, glaze radios, quantity, an add to cart that really adds, and three spec disclosures at prose width"},{"name":"cart-contents","file":"src/components/cart-contents.tsx","renders":"The live cart: line items with tight-crop thumbnails, a working quantity stepper and remove, a gift note, an order summary whose total is set at display size and whose delivery charge is arithmetic, plus the designed empty state you reach by emptying it"},{"name":"collection-split","file":"src/components/collection-split.tsx","renders":"Process photograph beside four numbered making stages with real timings, split seven columns to five, with an optional link to the long version"},{"name":"firing-calendar","file":"src/components/firing-calendar.tsx","renders":"The kiln year: a reading block at prose width whose next firing date is set at display size, above the four firings in the full-bleed hairline plate"},{"name":"promises","file":"src/components/promises.tsx","renders":"Dispatch, packing and returns in the full-bleed hairline plate with lucide marks - the quiet density register on every route it appears on, with an optional link to the full terms"},{"name":"delivery-rates","file":"src/components/delivery-rates.tsx","renders":"The free-delivery threshold at display size beside a real table of costs and transit windows by destination, split five columns to seven"},{"name":"returns-faq","file":"src/components/returns-faq.tsx","renders":"Four native disclosures at prose width covering returns, breakages and glaze swaps, carrying the #returns anchor"},{"name":"small-print","file":"src/components/small-print.tsx","renders":"The shop's small print in three clauses under a rule - who you buy from, when an order is an order, what happens to your email address - unboxed at caption size rather than in a fourth plate"},{"name":"reviews","file":"src/components/reviews.tsx","renders":"Three named buyer quotes to a page in a full-bleed plate, the first at display size, each tied to the piece that buyer owns, with one average-rating line; on /product the list is the reviews of the piece being sold"},{"name":"newsletter","file":"src/components/newsletter.tsx","renders":"Restock-list signup on the sage accent surface, with a labelled email input and the theme s one validation style"},{"name":"page-header","file":"src/components/page-header.tsx","renders":"Editorial opener with eyebrow, title, optional lede and an optional mono spec column in the right third, for pages that are not the shop front"},{"name":"site-footer","file":"src/components/site-footer.tsx","renders":"Four-column footer with studio address, three nav groups and a firing-date legal bar"},{"name":"ui/button","file":"src/components/ui/button.tsx","renders":"Button, ButtonLink and buttonClasses - three variants, three sizes, hover and press from the state ramp, a disabled state, a pending state that holds the pressed colour and carries aria-busy so a submit cannot fire twice, and 2px of optical padding trimmed on the icon side"},{"name":"ui/badge","file":"src/components/ui/badge.tsx","renders":"Badge in three appearance variants, including the opaque surface variant used over photographs, and Eyebrow on the label step"},{"name":"ui/container","file":"src/components/ui/container.tsx","renders":"The single gutter rhythm at prose, default and wide widths"},{"name":"ui/field","file":"src/components/ui/field.tsx","renders":"The form family every Kiln control is built from - TextField, SelectField, TextareaField and the Field shell. One label, one optional read-only sentence and one message line whose precedence is error then success then description, so a form that is wrong in one place no longer reveals every sentence it holds. Both validation modes survive: the browser-driven :user-invalid style by default, and a declared invalid where JavaScript owns the verdict"},{"name":"ui/product-card","file":"src/components/ui/product-card.tsx","renders":"One bay of the shelf: a white lit surface holding a square photograph, name, price and stock chip, with an optional href so a sold-out piece is not a click target"},{"name":"ui/catalogue","file":"src/components/ui/catalogue.ts","renders":"No markup, the six canonical photographed products shared by product-grid and collection-browser, with integer unit prices, glaze labels, stock and per-surface detail-link eligibility"},{"name":"ui/promises","file":"src/components/ui/promises.ts","renders":"No markup, the three dispatch, packing and returns promises written once: the hero's fact row reads their titles, the promises plate reads titles and bodies, and the buy panel and cart summary set the one returns phrase inside their own sentence"},{"name":"ui/reviews","file":"src/components/ui/reviews.ts","renders":"No markup, the eighteen published reviews written once, each keyed to a catalogue piece: the reviews block walks them, the buy panel reads its count and mean from the same rows, and no review count is typed anywhere"},{"name":"ui/icons","file":"src/components/ui/icons.tsx","renders":"KilnMark, the theme's only hand-drawn SVG"},{"name":"ui/cart-store","file":"src/components/ui/cart-store.ts","renders":"No markup, all of the state: the priced fixture table, the lines, the totals arithmetic, localStorage persistence and the useCart hook every cart surface reads"},{"name":"ui/cart-drawer","file":"src/components/ui/cart-drawer.tsx","renders":"The header's cart as a real modal sheet: a live count in the trigger's own name, the lines and the computed subtotal from the store, one action that names where it goes, and its own empty state"},{"name":"ui/mobile-nav","file":"src/components/ui/mobile-nav.tsx","renders":"The narrow-width nav: a sheet from the left, opened by the one control in the bar that is not the cart, with aria-current on the route a reader is already on"},{"name":"ui/mode-toggle","file":"src/components/ui/mode-toggle.tsx","renders":"The navbar light/dark switch: Sun and Moon swapped in CSS, aria-pressed derived from the observed theme root, paired light/dark classes, and a remembered standalone choice"},{"name":"ui/accordion-disclosure","file":"src/components/ui/accordion-disclosure.tsx","renders":"The disclosure row three files used to retype: a summary on a hairline, a chevron that turns on the theme's own 250ms clock, and the answer tweened open through ::details-content. Escape inside an open panel collapses it and hands focus back to the summary, which is the row native details never gave"},{"name":"ui/sheet-drawer","file":"src/components/ui/sheet-drawer.tsx","renders":"The edge-anchored modal panel the cart and the narrow-width nav both are: a scrim that dismisses, a trapped Tab, a locked page, Escape, and focus back on the control that opened it. Four edges, three depths, and the panel arrives with the 16px and the 400/350 the theme's one considered transition has always used"},{"name":"ui/quantity-stepper","file":"src/components/ui/quantity-stepper.tsx","renders":"One bounded quantity control for the cart line and the product page, replacing a select of six options on one and a minus-plus cluster on the other. A real number input between two controls: it posts, the browser clamps it, both bounds disable, and the press that reaches one moves focus to the figure before the button under it goes dead"},{"name":"ui/variant-selector","file":"src/components/ui/variant-selector.tsx","renders":"The glaze axis as native radios in a fieldset, so a choice posts with no JavaScript. A chosen chip takes the sage ground and a tick; an option that is out takes the muted ground and carries the firing it comes back in inside its own label, where a screen reader meets it"},{"name":"ui/rating-input","file":"src/components/ui/rating-input.tsx","renders":"The rating a buyer sets in the review form: five stars, whole marks only, and a No rating option first so ArrowLeft takes it back off. Filled is a fill and not a colour, so the marks read in a greyscale capture"},{"name":"ui/filter-chip","file":"src/components/ui/filter-chip.tsx","renders":"One active-filter receipt in the collection browser: the value drawn as a pill and a separately named remove control, so the words on screen are the words in the accessible name. Removing one lands focus on the select that set it"},{"name":"ui/search-input","file":"src/components/ui/search-input.tsx","renders":"The shelf search over name, piece family and glaze: a real type=search with a drawn label, a clear that returns focus to the field before it empties, and a polite count that is the one sentence the whole filter bar announces through"},{"name":"ui/pagination","file":"src/components/ui/pagination.tsx","renders":"The reviews strip: mono page targets, ends that disable rather than wrap, one elision either side of a long run, and a live range sentence that says what the last move did"},{"name":"ui/form","file":"src/components/ui/form.tsx","renders":"The submit flow behind the restock list and the review form: validation on submit only, noValidate set from script so the JavaScript-free path still posts, values preserved through a refusal, a disabled and aria-busy fieldset while the request is out, and a focus destination at both ends"},{"name":"ui/form-error-summary","file":"src/components/ui/form-error-summary.tsx","renders":"The counted panel a refused review lands on, with one link per field. It is drawn as the theme's card with the clay edge rather than as a second red band, because Kiln declares no critical token"}],"tokens":{"--background":"#f6f1ea","--foreground":"#231c16","--card":"#ffffff","--card-foreground":"#231c16","--primary":"#b34a2c","--primary-hover":"#9f3718","--primary-active":"#8a2500","--primary-foreground":"#ffffff","--secondary":"#eee5d8","--secondary-foreground":"#44372e","--muted":"#f1eae0","--muted-foreground":"#6a5c50","--accent":"#e8ecdc","--accent-foreground":"#8f3a1f","--border":"#e3d8c8","--ring":"#b34a2c","--input-border":"#887d72","--surface-tint":"#f0e6dd","--shadow-color":"#1f1307","--radius":"0.375rem","--font-display":"var(--font-newsreader), \"Iowan Old Style\", Georgia, \"Times New Roman\", serif","--font-text":"var(--font-karla), ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif","--font-sans":"var(--font-text)","--font-mono":"var(--font-spline-mono), ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace","--text-d1":"1.5rem","--leading-d1":"1.25","--tracking-d1":"-0.01em","--text-d2":"2rem","--leading-d2":"1.2","--tracking-d2":"-0.01em","--text-d3":"clamp(2rem, 1.5rem + 2.5vw, 2.665rem)","--leading-d3":"1.15","--tracking-d3":"-0.02em","--text-d4":"clamp(2.5rem, 1.75rem + 3.75vw, 3.553rem)","--leading-d4":"1.08","--tracking-d4":"-0.025em","--text-lead":"1.125rem","--leading-lead":"1.6","--text-body":"1rem","--leading-body":"1.55","--text-small":"0.875rem","--leading-small":"1.45","--text-caption":"0.75rem","--leading-caption":"1.4","--tracking-label":"0.1em","--display-weight":"500","--duration-feedback":"150ms","--ease-feedback":"cubic-bezier(0, 0, 0.2, 1)","--duration-state":"300ms","--ease-house":"cubic-bezier(0.22, 1, 0.36, 1)","--duration-disclosure":"250ms","--duration-panel-open":"400ms","--duration-panel-close":"350ms","--ease-swap":"ease-in-out","--scrim":"color-mix(in oklab, var(--shadow-color) 38%, transparent)","--sheet-shift":"1rem 0"},"tokensDark":{"--background":"#171310","--foreground":"#f3ece2","--card":"#201a16","--card-foreground":"#f3ece2","--primary":"#bc5234","--primary-hover":"#ab4325","--primary-active":"#9b3313","--primary-foreground":"#ffffff","--secondary":"#2c211a","--secondary-foreground":"#e7ded1","--muted":"#241e19","--muted-foreground":"#ab9d8f","--accent":"#24271b","--accent-foreground":"#f0a184","--border":"#3a2f27","--ring":"#bc5234","--input-border":"#81766c","--surface-tint":"#2a2118","--shadow-color":"#070200","--radius":"0.375rem","--font-display":"var(--font-newsreader), \"Iowan Old Style\", Georgia, \"Times New Roman\", serif","--font-text":"var(--font-karla), ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif","--font-sans":"var(--font-text)","--font-mono":"var(--font-spline-mono), ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace","--display-weight":"400","--text-d1":"1.5rem","--leading-d1":"1.25","--tracking-d1":"-0.01em","--text-d2":"2rem","--leading-d2":"1.2","--tracking-d2":"-0.01em","--text-d3":"clamp(2rem, 1.5rem + 2.5vw, 2.665rem)","--leading-d3":"1.15","--tracking-d3":"-0.02em","--text-d4":"clamp(2.5rem, 1.75rem + 3.75vw, 3.553rem)","--leading-d4":"1.08","--tracking-d4":"-0.025em","--text-lead":"1.125rem","--leading-lead":"1.6","--text-body":"1rem","--leading-body":"1.55","--text-small":"0.875rem","--leading-small":"1.45","--text-caption":"0.75rem","--leading-caption":"1.4","--tracking-label":"0.1em","--duration-feedback":"150ms","--ease-feedback":"cubic-bezier(0, 0, 0.2, 1)","--duration-state":"300ms","--ease-house":"cubic-bezier(0.22, 1, 0.36, 1)","--duration-disclosure":"250ms","--duration-panel-open":"400ms","--duration-panel-close":"350ms","--ease-swap":"ease-in-out","--scrim":"color-mix(in oklab, var(--shadow-color) 62%, transparent)","--sheet-shift":"1rem 0"},"compatibility":{"next":">=15","react":">=19","tailwind":">=4"},"dependencies":["lucide-react"],"editions":{"react":{"source":"src","runtime":"react"},"html":{"source":"html/src","output":"html/dist","engine":"nunjucks","runtime":"vanilla","buildRequired":false}},"ui_library":{"version":"1.5.2","files":["html/src/js/_tt/accordion-disclosure.js","html/src/js/_tt/dialog.js","html/src/js/_tt/field-ids.js","html/src/js/_tt/field.js","html/src/js/_tt/filter-chip.js","html/src/js/_tt/focusables.js","html/src/js/_tt/form-error-summary.js","html/src/js/_tt/form.js","html/src/js/_tt/layer.js","html/src/js/_tt/mode.js","html/src/js/_tt/pagination.js","html/src/js/_tt/pending.js","html/src/js/_tt/presence.js","html/src/js/_tt/quantity-stepper.js","html/src/js/_tt/rating-input.js","html/src/js/_tt/reduced-motion.js","html/src/js/_tt/search-input.js","html/src/js/_tt/sheet-drawer.js","html/src/js/_tt/variant-selector.js","src/components/ui/_tt/accordion-disclosure.axes.ts","src/components/ui/_tt/accordion-disclosure.fixtures.ts","src/components/ui/_tt/accordion-disclosure.tsx","src/components/ui/_tt/capture.ts","src/components/ui/_tt/field-ids.ts","src/components/ui/_tt/field.axes.ts","src/components/ui/_tt/field.capture.ts","src/components/ui/_tt/field.fixtures.ts","src/components/ui/_tt/field.tsx","src/components/ui/_tt/filter-chip.axes.ts","src/components/ui/_tt/filter-chip.fixtures.ts","src/components/ui/_tt/filter-chip.tsx","src/components/ui/_tt/focusables.ts","src/components/ui/_tt/form-error-summary.axes.ts","src/components/ui/_tt/form-error-summary.fixtures.ts","src/components/ui/_tt/form-error-summary.tsx","src/components/ui/_tt/form.axes.ts","src/components/ui/_tt/form.fixtures.ts","src/components/ui/_tt/form.part.tsx","src/components/ui/_tt/form.ts","src/components/ui/_tt/host.ts","src/components/ui/_tt/ids.ts","src/components/ui/_tt/layer.ts","src/components/ui/_tt/pagination.axes.ts","src/components/ui/_tt/pagination.fixtures.ts","src/components/ui/_tt/pagination.tsx","src/components/ui/_tt/presence.ts","src/components/ui/_tt/quantity-stepper.axes.ts","src/components/ui/_tt/quantity-stepper.capture.ts","src/components/ui/_tt/quantity-stepper.fixtures.ts","src/components/ui/_tt/quantity-stepper.tsx","src/components/ui/_tt/rating-input.axes.ts","src/components/ui/_tt/rating-input.capture.ts","src/components/ui/_tt/rating-input.fixtures.ts","src/components/ui/_tt/rating-input.tsx","src/components/ui/_tt/recipe.ts","src/components/ui/_tt/reduced-motion.ts","src/components/ui/_tt/search-input.axes.ts","src/components/ui/_tt/search-input.capture.ts","src/components/ui/_tt/search-input.fixtures.ts","src/components/ui/_tt/search-input.tsx","src/components/ui/_tt/sheet-drawer.axes.ts","src/components/ui/_tt/sheet-drawer.fixtures.ts","src/components/ui/_tt/sheet-drawer.tsx","src/components/ui/_tt/variant-selector.axes.ts","src/components/ui/_tt/variant-selector.capture.ts","src/components/ui/_tt/variant-selector.fixtures.ts","src/components/ui/_tt/variant-selector.tsx"]},"license":"standard","undertone":"66deg","accent_mode":"second-hue","known_limitations":["The cart is a client store in localStorage (key kiln-cart), not an inventory system. It is seeded from the shipped fixture on a first visit - a demo cart that opens empty demonstrates nothing - and everything after that is real: add, change quantity, remove, empty it and the designed empty state is what you get. Stock is a per-piece ceiling on the stepper rather than a ledger, and no price is ever taken from the client: the checkout POST carries one line field per line as id:glaze:quantity. Wire src/components/ui/cart-store.ts to your backend and every component above it keeps working.","The cart and the narrow-width nav are one part now - ui/sheet-drawer, a real modal layer with Escape, a focus trap, a focus return and a scroll lock - which retires the 2.2.0 decision to leave both as native details panels. The cost is two client islands where there were one and a half: ui/cart-drawer and ui/mobile-nav. The HTML edition ports the nav sheet and drops the cart entirely, because that edition has no store; the ledger in AGENTS.md carries the row.","The light/dark toggle treats an embedded theme root as authoritative and ignores storage there, so a remembered choice cannot overrule the marketplace preview. Standalone, a stored choice is applied after hydration, so a visitor may see the rendered mode for one frame. Closing that gap needs a blocking script in the host app's document head. Kiln's dark mode is a class, never the dark: variant: nothing in this catalogue declares @custom-variant dark, so dark:hidden would follow the operating system rather than the toggle. Per-mode markup uses [.dark_&] instead.","The demo ships one Ridge tumbler product page. On /collection only that piece is linked; the other five cards are correctly non-interactive because sending a bowl or jug to the tumbler detail would be a task failure. The older home and related shelves retain their broad /product links for compatibility, while the sold-out stem vase has no href anywhere. A second detail page requires a second three-image shoot. For the same reason the cart holds three of the six pieces: a cart line needs the shipped tight thumbnail crop.","Collection filtering and sorting are local React state over the six shipped fixtures. They are synchronous, do not persist across a full reload, do not query inventory, and never write the cart. Replace src/components/ui/catalogue.ts with the buyer's catalogue adapter when the inventory grows beyond this honest static boundary.","Checkout is a POST target (/checkout), not a page. Everything after the button belongs to the buyer's payment provider, and a demo checkout would have to render a card field that takes no card and an order number for an order nobody placed - the simulated state AGENTS.md invariant 7 exists to forbid. The cart's summary form posts the real cart there and stops.","The disclosure height tween and the cart-drawer open/close are authored with no JavaScript, so both degrade by browser rather than by feature detection. A browser without interpolate-size keeps the instant open the theme shipped before 2.6.0 - nothing is hidden, it simply arrives without the 250ms - and a browser without @starting-style shows the drawer and the mobile menu appearing rather than easing in. Both are the correct degradation; neither is worth a script. The status line's text swap ships its entrance only, because its exit would need a timeout to hold a node React has already replaced.","The product photographs are AI-generated and graded, not a real studio shoot. Every prompt and the grade spec are recorded in src/assets/README.md so a buyer can regenerate or reshoot to the same numbers.","The restock list and the review form both stand in for a request that does not exist: onValid waits 700ms and resolves, which is one arrow function to replace. The submitting and success states are real states a visitor reaches, and neither is a cart control - every cart control still writes the store and reads back out of it. The review form's rating is optional, because ui/rating-input exposes no required and a native constraint gate is what the whole submit flow runs on."],"hero_archetype":"A8","content_register":"Editorial","design":{"typefaces":{"display":"Newsreader","text":"Karla","mono":"Spline Sans Mono"},"typeRatio":1.333,"signature":"The shelf: a full-bleed hairline plate of white bays, one square photograph per bay, running past the container to both page edges across the shop, collection, product, making and component routes.","surface_kind":"commerce","built_against":"2026-08-28","levers":{"theme":"light","density":"airy","accent_mode":"duotone","motion":"restrained","borders":"hairline","radius":"small","texture":"none","shadow":"none"},"icons":{"set":"lucide","stroke":1.5,"ladder":["size-3.5","size-4","size-5"],"contexts":{"nav":"glyph","panel_header":"mark","fact_row":"none","feature_grid":"none"}},"disclosure":{"helper_line":"one-clause","sentence":"hint","paragraph":"details","parallel_views":"tabs","secondary_object":"drawer"},"cta":{"header":"quiet","hero":"filled","repeats":["fold","end"],"mobile":"inline"},"colour_budget":{"brand_max_pct":10,"concentration_pct":70,"regions":[]}}}