Developers · Theme format
The theme format
A theme is JSON and CSS. No build step, no framework, no server to run. The same function that decides whether your theme installs on our side runs in the CLI and the editor, so a clean check there is a clean upload. For every node, component and data source, see the theme reference.
Two constraints
Know these before you start
Both are why a theme from somebody we have never met can be installed on a live shop at all.
- A theme ships no JavaScript. None, in any form. A section is data and our storefront draws it, so there is nothing to sandbox. Anything interactive is a component you place by name, and several take their layout from your theme, so an accordion in your theme looks like your accordion. If you need a behaviour that is not in the list, tell us: that is a gap on our side, and it is how the list grows.
- Styling is plain CSS over our tokens, not Tailwind. Write
var(--shop-accent),var(--shop-font-heading),var(--shop-radius). Your theme.json sets the defaults, and the merchant can change any of them without touching your CSS.
Layout
The folder
This is what the workspace, the CLI and a zip upload all use. Keeping everything in theme.json alone also works; the folders are for your comfort, not a second format.
harbour/ theme.json identity, tokens, theme settings, plugins theme.css styling for the whole theme sections/<name>.json one file per section: its form, its data, its markup, its CSS templates/<page>.json one file per page: which sections, in which order AGENTS.md the performance rules, for coding assistants README.md yours
No other files are accepted: no images, fonts or scripts in the folder. A theme holds at most 60 sections and 2 MB in total.
Identity
theme.json
The top of the theme. The $schema line gives you completion and inline help in VS Code and most editors, from a schema generated from the validator itself.
{
"$schema": "https://whizzycommerce.com/schema/theme-v1.json",
"dsl": 1,
"slug": "harbour",
"name": "Harbour",
"version": "1.0.0",
"description": "Big photography and quiet type for homeware shops.",
"author": { "name": "Tide Studio", "url": "https://tide.example", "email": "help@tide.example" },
"docsUrl": "https://tide.example/harbour",
"specs": { "responsive": true, "rtl": false },
"plugins": { "blog": "^2.0.0" },
"tokens": { "colorAccent": "#1f3a5f", "radius": "0" },
"settings": [],
"migrations": {}
}| Field | What it is |
|---|---|
slug | Lower case letters, digits and dashes. It is permanent, and it prefixes your section types. |
version | Semver. See Releases for what each kind of version may change. |
plugins | Plugins the theme lays out for, with a version range. See Plugins below. |
tokens | Your defaults for the design tokens. |
settings | Theme-wide choices the merchant makes once per shop. |
migrations | Renames of sections, settings and tokens, for shops moving from an older version. |
Design
Tokens and settings
Tokens are the shop's colours, type and shape. Settings are choices about layout. Both have your defaults, and the merchant can change both in the shop's Design screen.
| Token | CSS variable | Default |
|---|---|---|
colorBg | var(--shop-bg) | #ffffff |
colorFg | var(--shop-fg) | #0f1115 |
colorAccent | var(--shop-accent) | #0b0f19 |
colorMuted | var(--shop-muted) | #6b7280 |
fontHeading | var(--shop-font-heading) | "Inter", system-ui, sans-serif |
fontBody | var(--shop-font-body) | "Inter", system-ui, sans-serif |
fontSizeBody | var(--shop-font-size-body) | 16px |
fontSizeHeading | var(--shop-font-size-heading) | 60px |
That is the first 8 of 27. The reference lists all of them.
"settings": [
{ "key": "homeLayout", "label": "Home layout", "type": "select", "default": "roomy",
"options": [{ "value": "roomy", "label": "Roomy" }, { "value": "compact", "label": "Compact" }] },
{ "key": "showNote", "label": "Show the delivery note", "type": "boolean", "default": true }
]A section reads a theme setting as { "get": "theme.homeLayout" } and its own settings as { "get": "settings.heading" }. Use a theme setting when the answer is one per shop, so the merchant changes it once rather than on every section.
Building blocks
Sections
A section is what a merchant adds to a page. It declares a form, the data it needs, its CSS and its markup as a tree of render nodes. You never build the form: declaring fields and blocks is what gives the merchant one.
{
"type": "harbour.ticker",
"label": "Ticker",
"category": "layout",
"description": "Short phrases across the page, in the accent colour.",
"fields": [
{ "key": "speed", "label": "Speed", "type": "select", "default": "slow",
"options": [{ "value": "slow", "label": "Slow" }, { "value": "fast", "label": "Fast" }] }
],
"blocks": [{
"type": "phrase", "label": "Phrase", "max": 6,
"fields": [
{ "key": "text", "label": "Text", "type": "text", "default": "Free delivery" }
]
}],
"css": ".ticker-band { display: flex; gap: 3rem; background: var(--shop-accent) }",
"render": [
{ "node": "box", "as": "section", "cls": "ticker-band", "children": [
{ "node": "slot", "children": [
{ "node": "text", "as": "span", "value": { "get": "block.text" } }
]}
]}
]
}- type is always
<your slug>.<name>. The file name is the part after the dot. - blocks gives the merchant an “Add phrase” button, capped at
max.slotdraws its children once per block, which is what makesblock.textmean this phrase. - render is a list of nodes, of these kinds: box, text, rich, img, link, each, if, slot, icon, money, component. Values are expressions such as
{ "get": "product.title" }or{ "lit": "Sale" }.
Data sources
A section declares what it needs, and the page fetches it before drawing anything. Two sections asking for the same list cost one query. A theme cannot write a query of its own.
"data": [
{ "as": "picks", "source": "collection-products",
"collection": { "get": "settings.collection" }, "limit": 8 }
],
"render": [
{ "node": "box", "cls": "picks-grid", "children": [
{ "node": "each", "in": { "get": "data.picks" }, "as": "item", "limit": 8,
"children": [
{ "node": "component", "use": "product-card", "props": { "product": { "get": "item" } } }
],
"empty": [
{ "node": "text", "as": "p", "value": { "lit": "Nothing to show here yet." } }
] }
]}
]Components
Components are the interactive and shop-aware parts: product-card, shop-logo, shop-menu, cart-link, search-box and more. You place them and style around them, with props where a component takes some. You cannot write one.
Pages
Templates
A template is the starting layout of one page: a list of placed sections with their settings. Merchants rearrange it afterwards.
[
{ "id": "hero", "type": "hero", "settings": { "heading": "Made to last" } },
{ "id": "picks", "type": "harbour.picks", "settings": { "heading": "New in", "limit": 4 } }
]| Handle | Page |
|---|---|
header | Header |
home | Home |
collection | Category page |
product | Product page |
footer | Footer |
blog | The blog's list of posts. Needs the blog plugin declared. |
blog-post | One blog post. Needs the blog plugin declared. |
A template named anything else (header, home, collection, product, footer, or a declared plugin's pages) is reported when you validate, rather than installing quietly and drawing nothing. A page holds at most 40 sections.
Dependencies
Plugins
Some pages belong to a plugin, such as the blog. To lay those pages out, declare the plugin in theme.json with the version range you built against.
"plugins": { "blog": "^2.0.0" }- Your templates may then use that plugin's pages and section types, such as
blog.indexandblog.post, and its components, such asblog-post-list. - Installing your theme installs the plugin first, so the pages you laid out exist.
- Ranges take the forms
^2.0.0,~2.1.0,>=2.0.0,2.1.0or*. A submission is blocked if the catalogue has no version in your range. - If you only style a plugin's pages with CSS, declare nothing. Your CSS already reaches them.
Styling
CSS, and how it is scoped
Write ordinary class selectors. At install, your CSS is scoped to your theme and every class name gets one prefix for the whole theme, so .card in your theme cannot collide with another theme's.
.grid, the rules of one restyle the other, and a submission with such a collision is blocked. Name classes after their section: .picks-grid, .lookbook-grid. Rules meant for every section belong in theme.css, which is shared on purpose.- Properties are checked against a list. Whatever is refused is reported when you validate, never dropped in silence.
url()may only point at our media CDN. Fonts come from the font tokens: name a Google font there and it is fetched for you, with no@importin your CSS.- A section's CSS is at most 40 kB, and the whole theme's at most 200 kB.
Pictures
Logos and images
Merchants upload their own logo, and an SVG logo stays a vector. It is filtered down to its drawing on upload and always placed in an img tag, so it is safe on any theme.
Place the shop's logo with { "node": "component", "use": "shop-logo" }. When your design needs a second version, such as a white logo on a dark bar, add a theme setting of type image and pass it as src:
{ "node": "component", "use": "shop-logo", "props": { "src": { "get": "theme.logoOnDark" } } }Size the box a picture sits in with CSS, not the picture. Photographs in your demo come from the sample catalogue on your development shop, so you do not ship any.
Speed
Performance rules
Google measures a shop on a mid-range phone on a slow network. These rules are the same text the CLI writes into AGENTS.md, and the release checks weigh your home page.
Rules for anyone editing this theme, including an AI assistant. Google measures the shop on a mid-range phone on a slow network, so that is the device to build for.
Pictures
- The picture at the top of the page (hero, first slide) gets
"priority": trueon itsimgnode. It is what Largest Contentful Paint times, and without it the browser waits to load it. Only that one: every other picture stays lazy. - Give every full-width picture a
mobileSrcfield for phones, a crop that works on a narrow screen, so a phone does not download a desktop banner. - Set
sizeson anyimgthat is not full width, for example"(min-width: 768px) 33vw, 50vw"for a three-column grid, so the browser picks a smaller file. - Give the box a picture sits in a fixed shape in CSS (
aspect-ratio), so the page does not jump when it arrives. - Do not put the main picture in a CSS
background-image: the browser finds it late and cannot pick a size for it.
Fonts
- At most two families, heading and body. Each one is a separate download.
- Leave the font tokens at the core stack if the design does not need a web font: that costs nothing at all.
Weight
- Keep
theme.csslean: no unused rules, no copies of the same rule per section. - Above the first screen, place only what is needed there. A long home page is fine; a first screen with a slideshow, a carousel and a video is not.
- Keep
eachlimits realistic. Twenty-four product cards on a home page is twenty-four pictures and a long page to render; eight is usually enough. - Avoid heavy CSS effects on large areas (
backdrop-filter, bigbox-shadow,filter: blur), which are slow to draw on a phone.
Checking
Run https://pagespeed.web.dev on the shop's home page and one product page, on Mobile. Aim for Largest Contentful Paint under 2.5 s and Cumulative Layout Shift under 0.1. Fix the first item Lighthouse lists before anything else.
Languages
Write it so it can be translated
Anything a merchant might reword should be a field with a default, not a literal string in the markup.
A field appears in the merchant's editor and in their translation screen, so your English heading becomes their Portuguese one. A lit value is stuck in English on every shop that installs your theme. Keep lit for things that are not words, such as a class name or a layout value.