Theme reference
Generated from the validator's own rules, so it cannot drift from what we accept. Point your editor at the JSON Schema and you get all of this as you type.
Nodes
What you can draw. A section's `render` is a list of these.
| box | A container. Becomes a div, or any layout or landmark tag you name. |
| text | A line of text, in the tag you name: a heading, a paragraph, a label. |
| rich | Richtext a merchant wrote, sanitised every time it is drawn. |
| img | A picture, drawn at the width its role says it will be drawn at: `role` is "main" (the default), "small" for a card or grid, "thumbnail" or "swatch". `mobileSrc` is a different picture for phones, a crop that works on a narrow screen. `priority: true` loads it at once, for the picture at the top of the page; in a repetition only the first one. |
| link | An anchor around anything. |
| each | Repeats its children over a list, under a limit it does not choose. |
| if | Draws one set of children or another, on a condition. |
| slot | Repeats its children once per repeatable block, with `block` in scope. |
| icon | One of the storefront's own icons. |
| money | An amount, formatted in the shop's display currency. |
| component | One of the components below. The only interactive thing a theme can place. |
Components
The interactive parts. You place and position them; you cannot write one. Those marked with slots take their layout from your theme.
| product-card | The ready-made card: picture, sale label, title and price; `showPrice: false` hides the price, `showVendor: true` shows the maker. Built from nodes over the product; build your own from `item.url`, `item.image`, `item.price` and the parts above for a quick add, swatches or a quick view. |
| add-to-cart | The button, with a quantity field in front of it. Inside a product-form it adds whatever is chosen; elsewhere pass `variantId`. Pass `quantity: false` for a one-press add, `size: "small"` for a compact button beside a row, and `label` to change the words. |
| variant-picker | The options, one group per axis (Size, Colour), with what cannot be bought greyed out. Inside a product-form. `style: "select"` makes each axis a dropdown; `option` narrows it to one axis. Buttons are radio inputs, so style the chosen one with `input:checked + span`. |
| custom-options | The product's fill-in fields: an engraving, a date, a file, gift wrap, whatever the merchant asked for, each with its extra price. Nothing on a product that asks for none. Any add-to-cart on the page reads it, so it need not sit inside a product-form. |
| variant-image | A variant's own picture as a small button, for a layout that lists variants as rows; pass `variantId`. Pressed, the product-gallery shows that picture; pressed again, or when another is pressed, the gallery goes back. Inside a product-form the picker does this on its own. Nothing when the variant has no picture. |
| search-box | The search field and its suggestions, which appear as the shopper types. `layout` is "header" (an icon that opens a bar, the default), "panel" (a sheet under the header) or "page" (a plain field); `focus: true` puts the cursor in it. |
| newsletter-form | Email capture, with the shop's own consent wording. |
| cart-drawer | The ready-made basket: a sheet from the right, opened by the basket icon and by adding something. Its width is the cartWidth token in theme.json. Built from the pieces below; build your own from them for anything else. |
| pagination | Next and previous, for a listing that runs past one page. |
| price | An amount with its compare-at and its currency handled. |
| account-menu | Sign in, or the customer's own links when they are. |
| accordion | Rows that open. You lay out the title and the body; a merchant adds the rows. Slots: title (per block), body (per block). |
| image-carousel | Slides the shopper can move through. You lay out one slide. Slots: item (per block). |
| popup-panel | Something that opens when pressed. You lay out the button and the panel. `openOn: "hover"` opens it on hover as a menu dropdown does; `span: "full"` hangs it across the whole header, for a mega menu; `plain: true` drops its own border and padding so your content is the panel. Slots: trigger, content. |
| shop-logo | The shop's own logo, at the size it was uploaded. Pass `src` (a theme setting of type image, say) to place a different one, a white version on a dark bar. |
| shop-menu | The ready-made menu: dropdowns on hover, or with `layout: "panel"` a button opening a full-width sheet with your `foot` slot under the columns. `trigger: "icon"` drops the word beside the hamburger and sets the button in the same square your other tools use. Built from popup-panel over `shop.menu`; build your own menus from the same, and a phone menu from a sheet with an accordion. Slots: foot. |
| cart-link | The basket, and how many things are in it, as a link to the cart page. |
| product-form | Holds which options are picked, for the parts inside it. Pass `product` (the page's `product`, or a list item). Lay out the page, a card or a quick view in its `content`. Slots: content. |
| product-gallery | The product's pictures, one large with thumbnails under it and a full-screen view. Pass `images` (product.images) and `title`. It moves to the chosen variant's picture (`variant.image`) and back when the choice is cleared. For your own layout, an each over product.images with img nodes. |
| product-price | The chosen variant's price, and what it was when it is on sale (an `s`). Inside a product-form. |
| stock-status | Sold out, on backorder, or only a few left, for the chosen variant. Nothing when it is simply in stock. |
| sheet | Something that opens over the page: `side` is "right" (the default), "left", "bottom" or "center" for a popup. `openOnAdd: true` opens it when something is added to the basket; `label` names it for screen readers. You lay out the `trigger` and the `content`, and style the content's outer box for its width. Slots: trigger, content. |
| sheet-close | Closes the sheet it sits in. Draws the close icon, or your own `content`. Slots: content. |
| cart-icon | The basket icon with its count, for a sheet's trigger. |
| cart-quantity | Fewer and more for one line. Pass `line: { get: "line.id" }` and `quantity: { get: "line.quantity" }`. |
| cart-remove | Takes one line out of the basket. Pass `line: { get: "line.id" }`; `label` overrides the wording, `style: "icon"` makes it the bin. |
| cart-clear | Empties the basket, after asking once. |
| discount-form | The discount code field and Apply, with why a code did not apply under it. |
| checkout-button | Goes to this shop's checkout, whichever kind it runs. `label` overrides the wording. |
| search-form | A plain search field that sends what is typed to a page as `q`. Pass `action` (a path on the shop, the blog's search say), `value`, `placeholder` and `label`. |
| login-form | Signing in with a six-digit code sent by email. On the login template. |
| register-form | Creating an account: name, email, and the marketing question. On the register template. |
| google-button | Continue with Google, when the shop offers it (draws nothing otherwise). `label` overrides the wording. |
| currency-switcher | The currencies this shop actually sells in, as a ready-made control. For your own, place currency-button over `shop.currencies`. |
| language-link | This page in another language. Pass `code` (a `shop.languages` item's code) and lay out what the shopper presses in `content`: the name, a flag. Slots: content. |
| currency-button | Shows the shop's prices in another currency. Pass `code` (from `shop.currencies`) and lay out what is pressed in `content`. Slots: content. |
| notice | A message screen readers announce: `tone` is "info" (the default) or "alert" for something that went wrong. Lay out the message in `content`. Slots: content. |
| language-switcher | The languages this shop is published in, as a ready-made control. For your own, place language-link over `shop.languages`. |
| social-links | The shop's own accounts, as icons. |
| blog-post-list | The ready-made list of posts, with its paging, built from nodes over `plugin.list`. `columns` is posts to a row, "1" (the default, each post across the width) to "3". On the blog's list template only. |
| blog-sidebar | The blog's sidebar: search, categories, recent posts, as the merchant set it up. |
| blog-post-body | The post itself: breadcrumbs, title, meta line, cover, body, gallery, tags and share links. On the post template only. |
| blog-post-nav | Previous and next post. |
| blog-related-posts | Posts related to this one. `heading` overrides the wording. |
| blog-related-products | Products the post is about. `heading` overrides the wording. |
Header and footer
In the header and footer the merchant's menu, languages and currencies are in scope under `shop`. Lay a menu out with ordinary nodes and popup-panel (on hover, across the header, or plain so your box is the panel), a phone menu with a sheet and an accordion, and a language or currency choice with language-link and currency-button. shop-menu is the ready-made menu, built from exactly these.
| shop.menu | The main menu the merchant built, for an `each`. Every item has label, url (empty for a heading), hasChildren, hasPanel, children (the same shape, up to three levels down when an entry shows its subcategories), mega (true when the merchant asked for a mega menu: draw each child as a column) and panel (image, link, html) when the merchant added a promo. |
| shop.languages | The languages the shop publishes: code, name (in its own language), current. For language-link. |
| shop.currencies | The currencies the shop sells in: code, current. For currency-button. |
| shop.labels.menu, shop.labels.seeAll | The words a menu needs, in the shopper's language. |
Products
On the product page `product` is in scope, and every product list a section asks for (newest, a category, related) holds the same shape. Lay a page or a card out with ordinary nodes; wrap what changes with the chosen size or colour in a product-form, with variant-picker, product-price, stock-status and add-to-cart inside it. product-card is the ready-made card, built from nodes over these fields.
| title, handle, vendor | As the merchant entered them. |
| url | The product's address, for a link. |
| image | The first picture, for an img. `images` is all of them, for an each or a carousel. |
| price, compareAt | The lowest price, and what it was; draw them with `money`. |
| onSale, soldOut | True or false, for an `if`: a sale label, a sold-out badge. |
| options | The option axes, each a name and its values: Size S M L. What variant-picker draws. |
| variants[].image | The combination's own picture, when the merchant gave it one; the gallery shows it while that variant is chosen. |
| specs | On the product page: the attributes the shop lists there, each a code, label, values and filterable. Empty when the shop lists none. |
| variants | Every variant: id, title, options, price, sku, available, stock (sold_out, backorder, low, in_stock) and, on the product page, lowText ("Only 3 left" in the shopper's language). |
| single, multi, related | On the product page: the lone variant when there is only one, whether there are several, and the related products as list items. |
| descriptionHtml, shortDescriptionHtml | The descriptions, for a `rich` node. On the product page. |
| shop.labels | The storefront's words in the shopper's language, everywhere: addToCart, soldOut, sale, backordered, options, quantity, viewProduct, noImage, close. |
The blog
On the blog's templates the plugin puts its page in scope as `plugin`: the list or the post, the sidebar, the merchant's display choices and the words. Lay the blog out over it with ordinary nodes and search-form for its search. The blog-* components are its ready-made pieces, built from exactly this.
| plugin.list.posts | On the list page: the posts, for an each. Each has title, url, cover, excerpt, categories (name, url, sep), meta (text, url, sep: the author, date and reading time the merchant chose to show) and hasMeta, hasCategories. |
| plugin.list.has, plugin.list.empty | Whether there are posts, and what to say when a search or a category has none. |
| plugin.list.paged, plugin.list.newerUrl, plugin.list.olderUrl, plugin.list.pageText | Paging: whether there is more than one page, the neighbouring pages, and "Page 2 of 5". |
| plugin.post | On the post page: the same fields as a list post, and html (the body, for a rich node), gallery, tags, share (name, url), trail (the breadcrumbs), previous and next (title, url), related (posts) and products (product-list items, for product-card or your own card), isDraft. |
| plugin.sidebar | search (whether to offer it), searchUrl and query for search-form; recent (title, url); categories (name, url, count, indent); tags (name, url); any. |
| plugin.settings, plugin.labels | The merchant's display choices (showBreadcrumbs, showPrevNext, showShareButtons, readMoreText) and the blog's words in the shopper's language. |
Signing in
The sign-in and create-account pages are the `login` and `register` templates, with `account` in scope. login-form, register-form and google-button are the shop's own, because they handle who somebody is; lay out everything around them. The pages' own sections are built from exactly these.
| account.google, account.googleError | Whether the shop offers Google sign-in, and why the last attempt did not finish, to show when not empty. |
| account.loginUrl, account.registerUrl | The two pages, each keeping where the shopper was going. |
| account.labels | The pages' words in the shopper's language: signIn, whySignIn, codeIntro, firstTime, createAccount, registerIntro, haveAccount, continueWithGoogle, or. |
Search results
The search results page is the `search` template, with the query and its results in scope as `search`. Place search-box with layout page for the field, product-card or your own card over search.results, and the categories for when nothing matched. The page's own section is built from exactly these.
| search.query, search.hasQuery | What was searched for, and whether anything was. |
| search.results, search.found | The products that matched, for an each (the same shape as any product list, so product-card or your own card), and whether there were any. |
| search.heading, search.countText | "Results for …" or "Search", and "12 products matching …" or "Nothing matched …", in the shopper's language. |
| search.collections | The shop's categories (title, url), somewhere to go when nothing matched. |
| search.prompt, search.noMatchHelp, search.browseHeading | The sentences around them: before a search, after one that found nothing, and over the categories. |
The basket
In a header or footer, and on the cart page (the `cart` template), the visitor's basket is in scope as `cart`. Lay it out with ordinary nodes and place sheet, cart-icon, cart-quantity, cart-remove, cart-clear, discount-form and checkout-button for what it does: a drawer from either side, a tray from the bottom, a popup in the middle, or a whole cart page. cart-drawer and the cart page's own section are the ready-made ones, built from exactly these parts.
| cart.itemCount | How many things are in the basket, counting quantities. |
| cart.empty | True when there is nothing in it. For an `if`. |
| cart.subtotal | The lines added up, before shipping; draw it with a `money` node. |
| cart.lines | The lines, for an `each`. Each has id, title, variant (or empty), image, url, quantity, unitPrice and total. |
| cart.discount, cart.discounts, cart.hasDiscount | What was taken off: the total, and one entry per promotion (name, amount, free) so a shopper can check each. |
| cart.shipping, cart.freeShipping, cart.needsDelivery | Delivery, and whether there is anything to deliver at all. |
| cart.tax, cart.taxIncluded, cart.total | Tax (included in prices or on top), and what it all comes to. |
| cart.code, cart.codeError | The discount code entered, and why it did not apply. |
| cart.conversionNote, cart.demoNote, cart.stockNotice, cart.paymentFailed | Sentences to show when they are not empty: prices shown in another currency, a demo shop, stock that changed, a payment that did not start. |
| cart.labels | The cart's words in the shopper's language: title, empty, startShopping, summary, product, quantity, each, subtotal, discount, discountCode, apply, shipping, free, freeOffer, tax, total, checkout, paymentFailed, viewCart, emptyCart, remove, close. |
Data
A section declares what it needs and the page fetches it before drawing anything, so two sections asking for the same list cost one query. A theme cannot write a query of its own.
| newest-products | The most recently added products in the shop. |
| collection-products | Products in a category, or the newest if none is chosen. |
| collections | The shop's categories. |
| product-variants | The variants of the product on this page. |
| product-images | The pictures of the product on this page. |
| related-products | Products that go with the one on this page. |
| metaobjects | Entries of a custom content type your theme defines. |
| cart-lines | Empty on purpose: pages are drawn once for every visitor. The basket is in scope as `cart` in the header and footer, which are drawn per visitor. |
| menu | A named menu the merchant built. |
| blog-posts | The blog's public posts, newest first; `category`, `tag` (handles) and `featured` narrow them. Empty without the blog plugin. |
| blog-categories | The blog's categories, as name, handle and url. |
Design tokens
Your theme.json sets the defaults; the merchant can change any of them. Read them in CSS as the custom property beside each one.
| colorBg | Page background. CSS: var(--shop-bg). Default: #ffffff |
| colorFg | Text. CSS: var(--shop-fg). Default: #0f1115 |
| colorAccent | Accent. CSS: var(--shop-accent). Default: #0b0f19 |
| colorMuted | Muted text. CSS: var(--shop-muted). Default: #6b7280 |
| fontHeading | Heading font. CSS: var(--shop-font-heading). Default: "Inter", system-ui, sans-serif |
| fontBody | Body font. CSS: var(--shop-font-body). Default: "Inter", system-ui, sans-serif |
| fontSizeBody | Body text size. CSS: var(--shop-font-size-body). Default: 16px |
| fontSizeHeading | Largest heading size. CSS: var(--shop-font-size-heading). Default: 60px |
| radius | Corner radius. CSS: var(--shop-radius). Default: 0 |
| maxWidth | Content width. CSS: var(--shop-max-width). Default: 1200px |
| cartWidth | Cart drawer width. CSS: var(--shop-cart-width). Default: 30rem |
| controlRadius | Button and field radius. CSS: var(--shop-control-radius). Default: 0 |
| btnBg | Button background. CSS: var(--shop-btn-bg). Default: #000 |
| btnFg | Button text. CSS: var(--shop-btn-fg). Default: #fff |
| btnBorder | Button border. CSS: var(--shop-btn-border). Default: #000 |
| imageMain | Main image, desktop. CSS: var(--shop-img-main). Default: 50vw |
| imageMainMobile | Main image, mobile. CSS: var(--shop-img-main-mobile). Default: 100vw |
| imageSmall | Grid image, desktop. CSS: var(--shop-img-small). Default: 25vw |
| imageSmallMobile | Grid image, mobile. CSS: var(--shop-img-small-mobile). Default: 50vw |
| imageThumbnail | Thumbnail, desktop. CSS: var(--shop-img-thumbnail). Default: 96px |
| imageThumbnailMobile | Thumbnail, mobile. CSS: var(--shop-img-thumbnail-mobile). Default: 72px |
| imageSwatch | Swatch, desktop. CSS: var(--shop-img-swatch). Default: 48px |
| imageSwatchMobile | Swatch, mobile. CSS: var(--shop-img-swatch-mobile). Default: 40px |
| checkoutSummarySide | Order summary on the. CSS: var(--shop-checkout-direction). Default: row |
| checkoutSummaryBg | Summary background. CSS: var(--shop-checkout-summary-bg). Default: #FFFFFF |
| checkoutSummaryFg | Summary text. CSS: var(--shop-checkout-summary-fg). Default: #000000 |
| checkoutFontHeading | Checkout heading font. CSS: var(--shop-checkout-font-heading). Default: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif |
| checkoutHeadingSize | Checkout heading size. CSS: var(--shop-checkout-heading-size). Default: 24px |
Expression roots
Where a `get` path may start. Which of these are in scope depends on where you are: `block` inside a slot, `item` (or whatever you named it) inside an each.
settings · theme · block · shop · page · product · collection · cart · search · account · plugin · customer · data
Tags
What a box and a text node may become.
box
div · section · header · footer · nav · main · aside · article · ul · ol · li · figure · figcaption
text
p · h1 · h2 · h3 · h4 · h5 · h6 · span · strong · em · small · blockquote · label
Icons
The storefront's own set. An icon node names one of these.
search · account · bag · bin · menu · close · chevron · clock · trend · folder · delivery · guarantee · returns · instagram · facebook · x · tiktok · youtube · pinterest · linkedin
Limits
Ceilings, all of them boring. A bundle is read on every request, so its size is a speed budget as much as a safety one.
| sectionsPerBundle | 60 |
| fieldsPerSection | 40 |
| blocksPerSection | 12 |
| fieldsPerBlock | 20 |
| templateSections | 40 |
| metaobjectTypes | 40 |
| fieldsPerMetaobject | 30 |
| cssBytes | 200,000 |
| sectionCssBytes | 40,000 |
| bundleBytes | 2,000,000 |
| dataRequestsPerSection | 6 |
| customSections | 30 |
| customSectionBytes | 60,000 |