Reference

Sections

What ships, and how each behaves.

SectionReactWhat it does
headerNav, mega menus, cart + menu triggers, localization
announcement-barSwiper slider with optional countdown
slideshowFull-bleed Swiper hero
featured-collectionProduct carousel with editable cards
collectionCollection grid — filtering, sorting, pagination
main-productVariants, subscriptions, add to cart
cartMain cart page
cart-drawerSlide-out cart
mobile-menuSlide-out nav
footerMenu / text / newsletter blocks
404, article, blog, collections, main-login, page, password, searchPlain Liquid

Section groups: header-group (header, announcement bar), footer-group (footer), overlay-group (cart drawer, mobile menu).

Every React section receives the same payload shape, whichever one it is:

1export default function AnySection({ id, settings, blocks, ...resources }) {
2 // `id` the section id — needed by useSectionState and useCarousel
3 // `settings` keyed by their schema ids, so snake_case
4 // `blocks` [{ id, type, settings, shopify_attributes }]
5 // resources whatever the .liquid opted into with `with_product` etc.
6}

See Adding a section for the Liquid side.

Navigation comes from the Shopify menu (Navigation in the admin), not blocks — merchants expect to manage it there, and the footer shares it. Blocks only add mega menu content on top. With no blocks you still get working dropdowns.

Each Mega menu block names the top-level item it opens under, matched on title, case-insensitively. Its own menu supplies the panel: top-level links become column headings, children become the links under them, plus an optional promo image.

Name an item that doesn't exist and nothing renders — which looks broken rather than mistyped, so in the editor it logs the titles that are available.

Announcement bar

Swiper-backed, one per header group. Each message block can carry a countdown with its own deadline. Selecting a block in the editor pins that slide.

Pick a collection; the section shows its products as a carousel. One source, deliberately — with no collection there is nothing to show, and in the theme editor the section says so rather than rendering an empty space.

The card is built from blocks: Product Card: Image / Title / Vendor / Price / Options / Button. Reorder them and the card reorders. The preset ships Image + Title + Price.

Product Card: Options puts variant pickers on the card. Each option — Size, Colour — gets its own control, so a product with several reads as several choices rather than one list of combinations.

Setting
StyleButtons or dropdowns
Show option namesThe Size / Colour labels above each control
Hide sold-out valuesOff by default — see below

Sold-out values stay visible and struck through by default, because a value that silently vanishes looks like a bug. A combination that cannot exist is a different thing and is always disabled: with Small selected, a Green that only comes in Large is not a choice.

Two things follow from adding this block:

  • The card image tracks the selection. Picking a variant that has its own photo swaps the card image to it. Variants often share the product image, so nothing changing is the normal case rather than a failure.
  • Quick add starts working for multi-variant products, because there is now an unambiguous variant to add.

It costs payload. Variant pickers need the variant list, and variants are the expensive part of a product — roughly 1.5 KB each, inline in the HTML, multiplied by every card in the carousel. The section includes them only when a Product Card: Options block is present, so a carousel without one is exactly as light as it was.

Product Card: Button does quick add through the shared cart store — opens the drawer, updates the header count. Without an Options block it only quick-adds products that have nothing to choose; anything with options links to the product page instead, because silently adding an arbitrary variant is worse than one more click.

Why not native theme blocks ({% content_for 'blocks' %})? They render Liquid server-side, and React's createRoot replaces everything inside its mount element. The two can't both own the card. Native blocks are still the right call for a Liquid-rendered section.

Collection grid

React grid with storefront filtering, sorting and three paging styles. Lives on the collection template.

Filtering never rebuilds a query. Every control navigates to a URL Shopify generated (url_to_add / url_to_remove), and the section re-fetches itself from that URL via ?section_id= — the Section Rendering API. Liquid stays the only thing that knows how to query a collection, and the URL stays real: shareable, bookmarkable, correct on refresh. If a fetch fails it falls back to a normal navigation, so the shopper still gets there.

Setting
ColumnsSeparate desktop and mobile counts, plus grid gap
FiltersSidebar or drawer — sidebar becomes a drawer below md either way
SortingShopify's own sort list, so labels stay translated
PaginationNumbered pages, Load more, or Infinite scroll

The card is built from blocks, the same set Featured collection uses: Product Card: Image / Title / Vendor / Price / Options / Button. Reorder them and every card in the grid reorders. Add none and the card falls back to image, title and price.

Adding Product Card: Options includes each product's variants in the page. That costs more here than in a carousel — a grid is a whole page of products rather than a handful — so it is worth being deliberate about. See Featured collection for what the block does.

Promotion blocks drop a tile into the grid at a 1-based position, with column and row span. A position past the last product appends rather than disappears — otherwise a filter that shrinks the results would silently swallow it.

Filters come from Shopify's Search & Discovery app. No filters configured there means an empty array, which the UI treats as normal rather than broken.

Values Shopify reports with a count of 0 render disabled rather than hidden, so the list doesn't jump around and the shopper can see why a combination is unavailable. Infinite scroll still renders a Load more button underneath — it's the fallback when IntersectionObserver never fires, and some people would rather ask.

Main product

Product-template only, and not addable — it has no preset, because it's the template's main section. Its default blocks live in templates/product.json.

The right column is built from blocks; the gallery isn't one, since it's the other half of the layout rather than a row in a stack.

Block
TitleOptional override, defaults to the product title
Vendor
PriceCompare-at, subscription price, per-delivery for prepaid plans
Product optionsButtons or dropdown; sold-out values struck through
Store availabilityPer-location stock — needs the API
Stock levelLow-stock threshold, exact count or just "Low stock"
QuantityRespects the variant's min / max / increment
Buy buttonCustom label, goes through the shared cart store
DescriptionThe product description
TextFree rich text (repeatable)
AccordionHeading + rich text, collapsible (repeatable)

Variant, quantity and plan state lives in the section, not the blocks — the price block and the buy button have to agree on which variant is selected, and they're siblings.

Two judgement calls in the stock block: untracked inventory never reports "0 left" (inventory_quantity is meaningless when the shop isn't tracking, so it says "In stock"), and available-but-zero reads as backorder rather than sold out, because that's a shop allowing oversell.

Slideshow

Swiper-backed. Per-slide text position (9-way grid), overlay colour and opacity. Section-level height, transition, speed, loop, autoplay, arrows, pagination, keyboard.

Three things are enforced in code rather than trusted to the setting, because each would otherwise be a control that silently does nothing:

  • Loop turns itself off when there aren't more slides than fit the view.
  • Fade forces one slide per view — Swiper requires it.
  • Autoplay is skipped under prefers-reduced-motion, and pauses while a slide is selected in the editor.

Cart

Both the main cart page and the drawer read the same store, so a change in one is reflected in the other. Both can show a free-shipping progress bar.

Menu, Text and Newsletter blocks in a responsive grid. Allowed in the footer group only, once.

This one is deliberately Liquid. The newsletter needs {% form 'customer' %}, which emits the hidden fields Shopify's contact endpoint expects and exposes form.posted_successfully? afterwards. React could post the same fields but couldn't read the result — the form object only exists inside the tag. Bonus: the footer works without JavaScript, and footer links stay in server-rendered HTML.