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" },
"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.
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.
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 |
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 |