File layout
<type>is 1–48 characters of lowercase letters, digits and hyphens ([a-z0-9-]{1,48}), and must match the file name.- The section is registered as
custom-<type>.theme/custom/sections/ingredient-grid.tsxbecomes the section typecustom-ingredient-grid. The prefix means a custom section can never collide with a platform section type, today or after a platform update. - You never register it by hand. On every preview compile and every publish, the pipeline generates
theme/custom/index.tsfrom the files on disk and merges it into the section and block registries. Do not writetheme/custom/index.tsyourself — it is regenerated.
What the file exports
The component receives the Builder2 render props:
sq is what lets one component serve both the editor and the live store:
sq.editorRef— attach it to the section’s outermost element so clicking the section in the editor preview selects it. It isnullin production.sq.blockRef(blockId)— attach it to each block’s outermost element when the section renders its own blocks, so a click selects that block. Returnsnullin production.sq.renderBlocks()— renders the section’s children when they are theme blocks (blocks registered with their own component) instead of section-local blocks.sq.isEditing—truein the editor preview. Do not use it to show content a customer would not see; the preview should look like the store.
Settings
settings is an ordered list. Input settings carry an id and store a value; header and
paragraph carry no id and only lay out the sidebar.
Every input setting takes
id, label, and optionally info (help text) and visible_if.
Blocks come in two forms, the same two Shopify has:
{ "type": "text" }— a theme block, resolved from the block registry and rendered by its own component throughsq.renderBlocks().{ "type": "ingredient", "name": "Ingredient", "settings": [...] }— a section-local block. Its schema lives in the section, it has no component of its own, and your section renders it fromblocks.
presets appears in the editor’s Add
section picker. A section without presets can only be placed by writing template JSON, and the
editor cannot remove it.
Placement. enabled_on or disabled_on (use one, not both) take templates and groups. A
section with neither is allowed on any page template but not in the header or footer groups; to
allow a group, name it: enabled_on: { templates: ["*"], groups: ["footer"] }.
Imports
A custom section may import only:
Anything else — including a new npm package — fails theme check with
import-resolves. Import
runtime values from the @/theme barrel rather than from a deeper path: @/theme is loaded once and
shared by every page, while a deep path bundles a second copy into your page, and a second copy of a
stateful module (the cart, the store data context) silently stops sharing state with the rest of the
store. Type-only imports (import type) are removed at build time and cost nothing.
A complete example
theme/custom/sections/ingredient-grid.tsx — a grid of a product’s active ingredients, each with an
optional image, the amount per serving and what it does. It uses section-local blocks, selects
correctly in the editor, and renders nothing for anything the merchant has not filled in.
theme/custom/sections/ingredient-grid.tsx
- An empty value renders nothing. No image means no image element, not an empty grey box; an
ingredient with no name is not listed; a section with no heading and no ingredients returns
null. An empty framed square reads to a shopper as a photo that failed to load. - The preset has no sample ingredients. Sample copy added from the picker would go live the moment the theme is published. The merchant or the agent adds real ingredients.
- Numbers from settings are clamped.
columnsis stored asunknown, so it is converted and kept between 2 and 4. StoreImage, not<img>ornext/image. It requests an image sized for the viewport and never preloads below-the-fold images.
templates/index.json:
templates/index.json
Rendering safely
- A section that throws does not take the page down. Each section renders inside its own error boundary and Suspense boundary, so a bug hides that one section. Still: check it in the preview.
- Mark interactive sections
"use client". Sections render in the browser and in the search-engine bake; a section that uses state or effects needs the directive. - Content should be in the HTML. Prefer native elements (
<details>for an accordion) over state that hides content until JavaScript runs — the page is baked for search engines and fast first paint. - Styling. Use Tailwind classes. The theme’s tokens are available as classes such as
bg-sq-surface,text-sq-fg,border-sq-border,rounded-sqand as CSS variables such asvar(--sq-bg), so a section follows the store’s colours and radius. Theme-wide CSS goes instyles/merchant.css.
Custom blocks
A theme block intheme/custom/blocks/<type>.tsx follows the same pattern: export const schema
(a BlockSchema: type, name, settings, optionally nested blocks and max_blocks) and a
default component receiving { id, settings, blocks, sq }. It registers as custom-<type> in the
block registry, and a section accepts it with blocks: [{ type: "custom-<type>" }] — or any
registered block with { type: "@theme" }.
Check before you publish
Run theme check —dtc theme check locally, or the check_theme tool.
TypeScript errors in changed custom files are warnings on a development theme and errors on
publish, so a section that does not type-check cannot go live.