The storefront is React (Next.js), so a custom section is written exactly the way PlatformDTC’s own sections are: a React component with its settings schema in the same file. Once the file exists in your theme, the section is a section type like any other — templates reference it, the dashboard editor lists it in Add section, and merchants edit its settings in the sidebar.

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.tsx becomes the section type custom-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.ts from the files on disk and merges it into the section and block registries. Do not write theme/custom/index.ts yourself — 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 is null in 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. Returns null in 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.isEditingtrue in 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 through sq.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 from blocks.
Presets make the section addable. A section with 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
What this section does deliberately:
  • 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. columns is stored as unknown, so it is converted and kept between 2 and 4.
  • StoreImage, not <img> or next/image. It requests an image sized for the viewport and never preloads below-the-fold images.
Use it from templates/index.json:
templates/index.json
Use your product’s real ingredients, amounts and claims — what a section says on the live store is a statement your business makes to customers.

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-sq and as CSS variables such as var(--sq-bg), so a section follows the store’s colours and radius. Theme-wide CSS goes in styles/merchant.css.

Custom blocks

A theme block in theme/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 checkdtc 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.