A theme is a single JSON file. It controls how a storefront looks — palette, display font, header menu, home banner and footer. It never contains products, categories, orders or settings, which is why one file works in any store.
Anyone can write one by hand. Nothing needs to be compiled.
The file
{
"format": "my-store.theme",
"version": 1,
"theme": {
"name": "Midnight",
"fontFamily": "Space Grotesk",
"colors": { "...": "9 tokens" },
"style": { "...": "shape, spacing and construction" },
"layout": [ { "id": "hero", "type": "hero", "data": {} } ],
"menu": [ { "label": "New Arrivals", "href": "/new-arrivals" } ],
"hero": { "...": "banner image and copy" },
"footer": { "...": "columns and small print" }
}
}
format must be exactly my-store.theme — it is what stops an unrelated
JSON file being imported. version is optional today; bump it only when a
future change makes old files unreadable.
Import rejects anything that fails validation, field by field, so a mistake
gives you a message like theme.colors.ink must be a hexadecimal color
rather than a broken shop.
Colours
All nine are required, all hex (#RRGGBB or #RGB).
| Token | What it paints | Weight |
|---|---|---|
ink |
text, dark buttons, footer, hero backdrop | heaviest — 63 uses |
accent |
brand highlight: logo dot, badges, hover states, newsletter band | 36 |
cream |
raised surfaces, light buttons | 23 |
clay |
image placeholders while photography is missing | 14 |
rust |
the "AUTUMN 26" eyebrow chip | 7 |
sand |
page background | 4 |
parchment |
text on rust |
4 |
dune |
the ticker strip under the hero | 3 |
gold |
hover state on the hero's primary button | 2 |
Two pairings carry most of the design: ink on sand for the page, and
accent for anything interactive. Get those right and the rest follow.
Watch the contrast pairs — the storefront assumes:
creamandparchmentread againstinkinkreads againstsand,creamandclay- white reads against
accent(badges and the newsletter band use white text)
An accent lighter than about #999 will fail that last one.
Font
"fontFamily": "Space Grotesk"
Any Google Fonts family name, letters, digits
and spaces only. The storefront loads weights 400 and 700, so pick a family
that has both. The name must match Google's exactly — DM Sans, not
DM-Sans.
Style
Colour is not a design. Two themes with the same shapes and spacing read as
the same theme repainted, which is why a theme also carries style.
Every field is optional; anything you leave out keeps the platform default, which is the original design.
| Token | Default | What it changes |
|---|---|---|
radiusPill |
9999px |
buttons, chips, badges — the biggest single lever |
radiusCard |
2px |
image wells and tiles |
radiusPanel |
8px |
panels and authored blocks |
imageAspect |
4 / 5 |
product crop; drives the page's silhouette |
controlHeight |
44px |
buttons and inputs |
typeScale |
1 |
multiplier on every display heading |
trackDisplay |
-0.02em |
tracking on large type |
trackLabel |
0.08em |
tracking on small uppercase labels |
labelCase |
uppercase |
none to stop shouting |
gutter / gutterWide |
20px / 40px |
page margins |
sectionY |
64px |
rhythm between sections — airy against dense |
shadowSm/Md/Lg |
soft | none for a flat design |
overlay |
rgba(0,0,0,.55) |
scrim over hero and tile imagery |
fontBody |
system-ui |
body face (the display face is fontFamily) |
Construction
Three of them are not values but a choice between layouts. Each names a component the storefront actually has, so the set is closed.
| Token | Options | |
|---|---|---|
headerLayout |
inline |
logo left, nav alongside |
centered |
wordmark over its own nav row | |
heroLayout |
overlay |
copy printed over a darkened image |
split |
copy beside the photograph | |
categoryShape |
square |
cards with the label inside |
circle |
a scrolling row of circles |
A worked example — premium beauty, structurally opposite to the default:
"style": {
"radiusPill": "2px",
"radiusCard": "0px",
"imageAspect": "3 / 4",
"controlHeight": "52px",
"trackLabel": "0.24em",
"sectionY": "112px",
"shadowSm": "none", "shadowMd": "none", "shadowLg": "none",
"headerLayout": "centered",
"heroLayout": "split",
"categoryShape": "circle"
}
Layout
The front page the design is built around, as a list of sections. Activating the theme applies it.
Every section, and what each one takes, is in Sections. That page matters more than this one if you are writing a theme for someone else: it says which sections read the shop's own products and categories, and which carry copy you have to supply.
Activating a theme overwrites the store's front page. That is deliberate — a theme you can switch to without the page changing is a recolour, not a theme — and the previous arrangement is kept so it can be put back.
A theme with no layout leaves the page alone.
Menu
Up to 12 items. Each needs a label (1–40 chars) and an href.
{ "label": "Sale", "href": "/new-arrivals", "highlight": true }
highlight renders that one link in the accent colour — the design uses it
for "Sale".
href must be a relative path starting with /. Absolute URLs are
rejected; a theme cannot point a shop's navigation at another site.
Real paths: /, /new-arrivals, /wishlist, /bag, /search,
/discover, /account.
Hero
Every field is optional; anything omitted falls back to a default.
| Field | Limit | Notes |
|---|---|---|
eyebrow |
80 | small chip above the headline |
title |
160 | \n becomes a line break |
subtitle |
400 | hidden if empty |
primaryLabel / primaryHref |
40 / 200 | main button |
secondaryLabel / secondaryHref |
40 / 200 | empty label hides the button |
url |
1024 | desktop banner, must be https:// |
mobileUrl |
1024 | portrait crop; falls back to url |
Leave url and mobileUrl as null for a portable theme. A URL only
resolves on the deployment it came from; elsewhere the banner shows its
placeholder and the colours, font and copy still work.
Footer
Up to 6 columns, each with up to 12 links. Same href rule as the menu.
"footer": {
"tagline": "Your style, your rules.",
"columns": [
{ "title": "SHOP", "links": [ { "label": "New Arrivals", "href": "/new-arrivals" } ] }
],
"legal": "© 2026 My Store",
"social": "Instagram · TikTok"
}
The storefront lays the columns out automatically, so 2 columns and 6 both look deliberate.
Bringing the whole design
A theme file describes a design, and a design is more than its palette. A version 2 file carries the front page's layout, the header and footer it was drawn against, and the pages it links to — so somebody else's shop ends up looking the way you built it, not merely sharing your colours.
Everything past theme is optional. A version 1 file, which has none of it,
still imports exactly as before.
{
"format": "my-store.theme",
"version": 2,
"meta": {
"author": "Studio Name",
"version": "1.0.0",
"description": "A tall editorial front page with a centred wordmark."
},
"theme": {
"name": "Editorial",
"fontFamily": "Jost",
"colors": { "ink": "#241F1C", "sand": "#FFFFFF", "...": "..." },
"style": { "radiusCard": "4px", "headerLayout": "centered", "...": "..." },
"layout": [
{ "id": "a", "type": "hero", "data": {} },
{ "id": "b", "type": "marquee", "data": { "text": "FREE DELIVERY" } },
{ "id": "c", "type": "productGrid", "data": { "limit": 8 } }
],
"menu": [{ "label": "New", "href": "/new-arrivals" }],
"hero": { "title": "NEW SEASON", "primaryLabel": "SHOP", "...": "..." },
"footer": { "tagline": "Made in Phnom Penh", "columns": [] }
},
"header": {
"name": "Editorial header",
"blocks": [
{ "id": "h1", "type": "logo", "data": {} },
{ "id": "h2", "type": "menu", "data": {} },
{ "id": "h3", "type": "cart", "data": {} }
],
"settings": { "sticky": true, "background": "sand" }
},
"footer": {
"name": "Editorial footer",
"rows": [{ "id": "r1", "columns": 3 }],
"blocks": [{ "id": "f1", "type": "heading", "data": { "text": "SHOP" } }]
},
"pages": [
{
"slug": "about",
"title": "About",
"blocks": [{ "id": "p1", "type": "heading", "data": { "text": "Our story" } }]
}
]
}
Note the two footers. theme.footer is the simple column footer every shop
has; the top-level footer is one built in the footer builder, blocks and
all. A file may carry either or both.
The five pages every shop has
A shop is created with five pages, and a theme should be designed against all five — they are what the standard menu links to:
| Page | Address | What a theme can do with it |
|---|---|---|
| Home | / |
Designed by the theme's layout — not by pages |
| Shop | /shop |
A normal page: put any sections in it, in pages |
| New Arrivals | /new-arrivals |
Fixed address, editable contents — the productListing block and anything around it |
| About Us | /about-us |
A normal page: put any sections in it, in pages |
| Cart | /bag |
Built-in route: the shopping bag |
Home, Shop and About Us are yours to arrange block by block, and so is every extra page a theme brings — every section on them is editable by the shop afterwards, so the merchant changes the words and the pictures while the design stays yours.
New Arrivals and Cart are system pages: their addresses are fixed and served
by the storefront's own route. They can still hold sections — New Arrivals is
the productListing block with anything the shop wants above and below it, and
an empty one draws what the route always drew. What a theme cannot do is
create a page at one of their addresses: /new-arrivals, /bag, /account,
/wishlist, /search, /discover, /product, /checkout and /orders are
reserved, so a file claiming one is refused.
So the smallest complete theme is a palette plus a layout for the home page.
A theme that wants the whole five-page shop to look like its design ships
layout for Home and pages for Shop and About Us, and may add as many
further pages as the design needs.
Designing a product listing
A listing — the trail and title, the chip bar, the filter sidebar and the grid of products — is four blocks, not one component. That is what lets a theme lay a shop page out instead of accepting the one arrangement the storefront used to have.
| Block | What it draws |
|---|---|
listingHeader |
the breadcrumb, the big title, the blurb, the product count |
listingFilterBar |
the row of quick chips, with the sort and the grid/list switch |
listingFilters |
the sidebar: Category, Size, Colour, Price |
listingResults |
the products themselves |
They pass nothing to each other. Everything they agree on is in the
address — ?filter=Hoodies&sort=Price ↑&view=list — so they can sit anywhere
on the page, at any depth, in any order, and still show the same filtered
products. Put the sidebar and the results in a columns block and the sidebar
is beside the grid; drop a banner between the chips and the products and
nothing else has to know.
The standard arrangement, which a theme can ship as any page's blocks:
[
{
"id": "head",
"type": "listingHeader",
"data": {
"heading": "EVERYTHING NEW",
"intro": "Landed in the last 30 days.",
"crumb": "New Arrivals",
"count": true
}
},
{
"id": "bar",
"type": "listingFilterBar",
"data": { "chips": true, "limit": 6, "sorting": true, "sticky": true }
},
{
"id": "body",
"type": "columns",
"data": {
"gap": 32,
"align": "start",
"columns": [
{
"id": "side",
"span": 1,
"blocks": [{ "id": "filters", "type": "listingFilters", "data": {} }]
},
{
"id": "main",
"span": 4,
"blocks": [
{
"id": "results",
"type": "listingResults",
"data": { "columns": 4, "limit": 0 }
}
]
}
]
}
}
]
Each block's own fields are in Sections. Useful ones:
listingResults takes a category and a columns count, so a theme can ship
a "Knitwear" page that is the same listing bound to one category;
listingFilters takes groups, a list of group names, so a page can show only
Size and Colour; and listingFilterBar takes group, which decides where the
quick chips come from.
productListing is all four in one section, for a page that needs a listing
rather than a layout. It takes the same words and switches, and it is what to
reach for inside a column or on a page that is mostly something else.
One limit worth knowing. New Arrivals and the Cart are system pages: their
addresses belong to the storefront, so a theme file cannot create a page at
/new-arrivals and hand over its arrangement. A theme ships this layout on its
own pages — a Shop page, a collection page — and a merchant who wants the same
on New Arrivals builds it there, where the identical blocks are in the palette.
A store that has never edited that page still gets the built-in listing.
What happens on import
- Nothing goes live. The theme arrives switched off, and its pages arrive as drafts. The shop decides when to show them.
- Names and addresses are made unique. A second copy of a theme becomes
"Editorial 2", and a page at
/aboutwhere one already exists becomes/about-2. Importing twice never overwrites anything. - A part that cannot be taken is reported, not fatal. A page written against a block this deployment does not have is skipped and named in the summary; the rest of the theme still arrives.
- Menus and placement are made local. A
menuIdnames a menu in the shop a theme was exported from, so on import every menu block is pointed at a menu the importing shop actually has. Header blocks that carry noalignare placed the way the built-in bar places them — logo left, menu centre, icons right — unless the file aligns at least one block, in which case the file is trusted exactly as written. - The shop's plan decides. A shop without custom headers, footers or pages gets the theme and its layout, and is told which parts were left out.
Building one
The quickest start is to build the design in a shop and press Export on the theme card: the file you get is exactly the format above, with that shop's header, footer and pages already in it. Edit it by hand from there.
Limits
| Part | Most |
|---|---|
| Front page layout | 40 sections |
| Header blocks | 40 |
| Footer blocks | 40, over 12 rows |
| Pages | 20 per file, 80 blocks each |
| Nested columns | 3 deep |
Header blocks are logo, menu, search, account, cart, wishlist,
text and link. Footer blocks are logo, heading, menu, text,
link and image. Page and layout blocks are the section types listed under
Layout above — including cardCarousel, postCardSlider,
beforeAfter, ingredientShowcase and custom, which a theme may use in its
layout and in its pages exactly like any other section. The console draws all
of them in its own preview, so a design built out of them looks like itself in
the gallery before anyone installs it.
Sections you write yourself
The blocks above cover the usual shapes. When a design needs its own markup — a category tile with the label over the photo, a row of icons, a split panel no block draws — a theme can carry a section of its own: your HTML, your CSS, this platform's data.
{
"id": "cards",
"type": "custom",
"data": {
"name": "Category cards",
"template": "<section class=\"cards\">…</section>",
"css": ".cards { display: grid; gap: 16px }",
"fields": [
{ "key": "headline", "label": "Headline", "type": "text", "default": "New this week" }
],
"values": {}
}
}
What a template may contain
| Tag | What it does |
|---|---|
{{ fields.headline }} |
Text the merchant edits in the console |
{% products category="Chairs" limit="8" %} … {% endproducts %} |
One copy per product |
{% categories limit="4" %} … {% endcategories %} |
One copy per category |
{% if fields.headline %} … {% endif %} |
Skipped when the field is empty |
Inside a products loop: {{ product.name }}, {{ product.price }},
{{ product.wasPrice }}, {{ product.image }}, {{ product.url }},
{{ product.badge }}, {{ product.category }}, {{ product.excerpt }},
{{ product.id }}. Inside a categories loop: {{ category.name }},
{{ category.count }}, {{ category.image }}, {{ category.url }}.
Prices arrive formatted in the shop's own currency. A loop draws at most 24
rows, whatever limit asks for.
Fields
Each declared field becomes a control in the console, so a merchant can change the words and pictures without opening your markup.
| Type | The merchant gets |
|---|---|
text |
A single-line box |
richText |
A multi-line box; its markup is kept |
image |
A box for a picture's address |
link |
A box for a URL |
color |
A colour picker |
Styling
The css field is yours. Every selector in it is scoped to the section
before it reaches the page, including inside @media, so a theme can never
restyle the rest of the shop by accident. @keyframes and @font-face are
left as written.
What is not allowed
A theme never runs code. Saving a section strips <script>, event handlers
such as onclick, <iframe>, <form> and javascript: links, and removes
@import from CSS. Every value substituted into a template is escaped, so a
product named <b>Chair shows those characters rather than becoming markup.
Limits: 20,000 characters of template, 10,000 of CSS, 24 fields.
A worked example
<section class="tiles">
<h2 class="tiles__title">{{ fields.headline }}</h2>
<div class="tiles__grid">
{% categories limit="4" %}
<a class="tile" href="{{ category.url }}">
<img class="tile__img" src="{{ category.image }}" alt="{{ category.name }}">
<span class="tile__name">{{ category.name }}</span>
<span class="tile__link">Explore</span>
</a>
{% endcategories %}
</div>
</section>
.tiles { padding: 72px 24px }
.tiles__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px }
.tile { position: relative; display: block; border-radius: 14px; overflow: hidden }
.tile__img { width: 100%; height: 320px; object-fit: cover }
.tile__name { position: absolute; left: 20px; bottom: 44px; font-size: 22px; color: #fff }
.tile__link { position: absolute; left: 20px; bottom: 20px; color: #fff; text-decoration: underline }
@media (max-width: 800px) { .tiles__grid { grid-template-columns: repeat(2, 1fr) } }
Put sections like this in a theme's layout for the front page, or in the
blocks of any page the theme brings, exactly like a built-in block.
Two ways to build one
Fork an existing theme — the quickest route. In the admin, open Themes, press + NEW THEME (it starts as a copy of the live design), change what you want with the preview beside you, save, then EXPORT.
Write the JSON directly — export any theme as a starting point, edit it, and import it back. Import creates it inactive, so you can check it in the gallery before making it live.
Before you ship a theme
- Import it into an empty store — one with no products and no uploaded images. That is how a stranger will first see it, and it catches themes that only look right against your own catalogue.
- Check the header, hero, product row, newsletter band and footer. The gallery card preview shows all of them at once.
- Check both surfaces: the storefront is a single responsive app, and the mobile layout uses the same tokens differently.
- Set
hero.urltonullunless you are shipping the theme inside the same deployment as the image. - Give it a distinctive
name. On import, a clash is suffixed (Midnight (imported)), never overwritten — but a clear name saves the recipient renaming it.
Limits at a glance
| Field | Rule |
|---|---|
format |
exactly my-store.theme |
name |
1–80 characters |
fontFamily |
1–80, letters/digits/spaces, a real Google Fonts family |
colors.* |
all 9 required, valid hex |
style.* |
optional; values are length-capped and may not contain ;{}: |
style.headerLayout |
inline or centered |
style.heroLayout |
overlay or split |
style.categoryShape |
square or circle |
layout |
≤ 40 sections; each type must be a known section |
menu |
≤ 12 items; label 1–40; href relative, starts with / |
footer.columns |
≤ 6; title 1–40 |
footer.columns[].links |
≤ 12; label 1–40; href relative |
hero.url, hero.mobileUrl |
https:// or null |