THEMES

Building a theme

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:

  • cream and parchment read against ink
  • ink reads against sand, cream and clay
  • 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.


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.


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 /about where 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 menuId names 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 no align are 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.url to null unless 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
Building a theme — Docs