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.isEditing — true 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. For images on the store’s CDN (cdn.platformdtc.com, where product photos and uploads live) it writes a srcset of ?width= URLs; the CDN answers each with an AVIF or WebP copy at that width, never larger than the original, so a 64px thumbnail downloads a few kilobytes instead of the full photo. Pass sizes (how wide the image renders) and, when you know them, width and height so the page does not shift while it loads. Pass priority only on the single largest image above the fold.
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 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.