Product
The product page, and the most composable section in Signal. Two columns: a media mosaic on the left, and on the right a buy column built entirely out of theme blocks — nothing in it is hardcoded.
Above both sits a breadcrumb trail. Below the page, the demo template adds two Recommendations rows, Recently viewed and an FAQ.
The composition model
Section titled “The composition model”The buy column is nothing but blocks. Every element a shopper reads — the title, the price, the variant picker, the buy button, the perks, the accordions — is a theme block you drag into whatever order the product needs. A size-led page for apparel and a spec-led page for gear are the same section with the blocks reordered.
Product (section)├── Breadcrumbs Home / collection / product├── LEFT .pdp-gal the media mosaic (iterates product.media)└── RIGHT .pdp-buy {% content_for 'blocks' %} ← merchant-controlled order ├── product-meta ├── product-title ├── price ├── product-description ├── variant-picker (+ the size guide dialog) ├── buy-buttons ← owns the <form> │ ├── selling plans / quantity rules / volume pricing / gift-card fields │ ├── quantity (static nested block) │ ├── add-to-cart (static nested block) │ └── accelerated-checkout (static nested block) ├── back-in-stock ├── product-inventory ├── pickup-availability ├── perk × 3 ├── share ├── product-details └── spec-listThat tree is the theme’s preset — what a merchant gets when they add the section fresh. The section accepts @theme and @app blocks, with no cap, so any theme block and any app block can go in the column too.
The blocks
Section titled “The blocks”| Block | What it contributes |
|---|---|
| Product meta | The line above the title: product type (or the collection), vendor, SKU |
| Product title | The <h1> |
| Price | Price, compare-at price, unit price, optional tax note |
| Rating | Stars from the standard reviews metafields, linking to a reviews anchor |
| Product description | A short lede (word-limited) or the full description |
| Variant picker | Swatches and size pills, plus the size guide |
| Quantity | The stepper. Nested inside buy-buttons |
| Buy buttons | The form, subscriptions, quantity rules, volume pricing, gift-card fields |
| Add to cart | The submit button and the wishlist heart. Nested inside buy-buttons |
| Accelerated checkout | Shop Pay and friends. Nested inside buy-buttons |
| Stock counter | How many are left in the run, as a number and a bar |
| Back in stock | Sign-up shown only while the selected variant is sold out |
| Pickup availability | Real in-store pickup from store_availabilities |
| Perk row | One reassurance line with an icon. Stack several |
| Trust badges | Secure-checkout line plus the payment methods the shop accepts |
| Share | X, Facebook, email and a copy-link button |
| Accordion | One collapsible row: the description, or custom text |
| Spec accordion | A spec table from Label | Value lines |
| Custom Liquid | App snippets or your own Liquid |
| Group · Text | Layout and free text |
Section settings
Section titled “Section settings”There are only four, because almost everything else is a block setting.
| Setting | Type | Default | What it does |
|---|---|---|---|
| Sticky add to bag | Checkbox | true | A slim bar slides up when the buy button scrolls out of view |
| Enable image zoom | Checkbox | true | Adds a zoom button to each image and opens a full-screen viewer |
| Add to bag label | Text | Add to bag | Label on the sticky bar’s button |
| Top spacing | Range 0–160 px | 16 | Top spacing |
| Bottom spacing | Range 0–160 px | 48 | Bottom spacing |
The section is disabled on the header and footer groups.
Breadcrumbs
Section titled “Breadcrumbs”Home / <collection> / <product title>.
The collection object is only populated when the shopper arrived through a collection URL — anyone landing from search, an ad or a direct link used to get “Home / Product”, which tells them nothing. Signal falls back to the product’s own first collection so the middle rung always says something.
The media mosaic
Section titled “The media mosaic”The gallery iterates product.media, not product.images, so images, Shopify-hosted video, YouTube/Vimeo embeds and 3D models all render — video_tag with controls (muted, inline), external_video_tag, and model_viewer_tag respectively.
It is a mosaic, not a carousel: a two-column grid you scroll, rather than a slider with thumbnails.
| Condition | Layout |
|---|---|
| Fewer than 3 media items | Every tile goes full width at 4:3 |
| 3 or more | 4:5 tiles in two columns, with the third tile spanning both columns at 16:11 |
| ≤ 980px | The page collapses to one column and the buy panel stops being sticky |
| ≤ 560px | The gallery itself collapses to one column |
Images are served responsive (400–1600w, 34vw desktop / 50vw below) with the first image loading eagerly and the rest lazily. A product with no media at all gets an apparel placeholder.
The first tile carries a flag when it applies: a dark Sold out flag when the selected variant is unavailable, otherwise an On sale flag when there is a compare-at price.
With enable_zoom on, every image tile gets a zoom button — revealed on hover on pointer devices, always visible on touch — and the page gets one full-screen <dialog>.
- Prev / next buttons and
←/→walk the whole set; both are hidden when there is only one image. - A counter reads n / total.
- Clicking the image toggles 2× zoom, anchored at the point you clicked via
transform-origin. - Opening paints the already-decoded thumbnail immediately and swaps in the 2400px file when it loads, so the stage is never empty while a large image downloads.
- Closing returns focus to the tile you opened from. Escape, the backdrop and the close button all work.
- The
<img>is created on first open — an emptysrcin the markup would ship a phantom request on every product page.
Variant images
Section titled “Variant images”When the shopper changes variant, if that variant has a featured_media the page smooth-scrolls the matching tile into view. Variants without a featured media leave the gallery alone.
Sticky add to bag
Section titled “Sticky add to bag”With sticky_atc on, a slim bar containing the product thumbnail, title, variant · price and a button slides in once the real action row (.pdp-act) has scrolled above the viewport. An IntersectionObserver watches that row, so the bar never appears while the real button is still on screen.
The bar’s button does not submit anything itself — it clicks the real add-to-cart button, so quantity, selling plan and every other form field are honoured. Its variant · price line is kept in sync by the same variant script that updates the main price.
The variant engine
Section titled “The variant engine”The section emits product.variants as JSON and resolves selections in the browser.
-
The variant picker renders one radio group per option, named
option-0,option-1, … -
On change, the script reads every checked radio, finds the first variant whose options match, and writes its id into the form’s hidden
idinput. -
It then updates, wherever those elements exist on the page: the price and compare-at price, the add-to-cart button’s label, price and disabled state, the sticky bar’s meta line, the stock counter, the back-in-stock form, the “Option — Value” labels, and the gallery scroll position.
-
Pickup availability is re-fetched for the new variant through the Section Rendering API, so it reflects the real per-variant, per-location answer rather than a cached one.
-
The URL’s
?variant=parameter is updated withhistory.replaceState, so the address bar is shareable without adding history entries.
Unavailable combinations
Section titled “Unavailable combinations”After every change the script re-walks every option value and asks what variant that value would produce:
- a value that produces no variant at all is disabled
- a value that produces an unavailable variant gets an
.outclass — rendered as a diagonal hatch with muted text
A sold-out size you have actually selected keeps the accent frame as well as the hatch: the hatch says unavailable, the frame says this is what you picked.
The stock counter
Section titled “The stock counter”The section emits a second JSON blob mapping each variant id to its inventory quantity — null when Shopify is not tracking inventory for that variant — because inventory is not part of the variants JSON, and the counter is a block the section cannot see.
The stock counter block then shows <n> left plus a bar, where the bar is quantity ÷ run size, clamped to 2–100%. Its stock_run setting (default 400) is the run size the bar is measured against, and stock_word / stock_note are the wording around the number.
The counter hides itself entirely when the selected variant is unavailable, untracked, or at zero.
Quantity rules and volume pricing
Section titled “Quantity rules and volume pricing”Both come from Shopify’s B2B quantity rules on the selected variant, and both are rendered by buy buttons:
- Quantity rules — when the variant has a minimum above 1, an increment above 1, or a maximum, a line states them, and the quantity input takes its
value,min,stepandmaxfrom the same rule. The stepper’s+/−buttons move by the increment and clamp to the rule. - Volume pricing — when the variant has
quantity_price_breaks, a small table lists the base tier (min+at the unit price) followed by each break as from N at its price.
The gift card case
Section titled “The gift card case”On a product where gift_card? is true, buy-buttons adds a “send this to the recipient” checkbox plus recipient email, name, message (200 characters) and a send-on date. The field names are the exact ones Shopify reads to deliver the card to the recipient instead of the buyer. The email becomes required only while the toggle is on, so a shopper buying a card for themselves is never blocked.
The size guide
Section titled “The size guide”Owned by the variant picker block, not the section — a block cannot read section settings, and the control belongs beside the option it explains.
| Setting | What it does |
|---|---|
size_guide_label (default Size guide) | The control’s wording, and the dialog’s heading |
size_guide_content (HTML) | When filled, the control opens a <dialog> containing it. Tables are supported |
size_guide_page (URL) | Used only when no content is set — the control becomes a plain link |
The control is rendered beside every option that is not treated as a swatch, which is what puts it next to Size rather than next to Colour. Which option names count as swatches comes from the global Swatches → Swatch option names setting (default color, colour, shade, tint).
Back in stock
Section titled “Back in stock”The back-in-stock block is its own <form>, which is why it must sit outside the product form — nested forms are invalid HTML and the browser silently drops the inner one.
It is hidden while the selected variant is available and revealed when it is not. Sign-ups are tagged back-in-stock and carry a note naming the exact product and variant, kept current as the shopper changes selection.
Submitting posts by fetch rather than letting Shopify’s customer form redirect to /contact — which would take the shopper off the product page and lose the confirmation with it. On success the form is replaced by the confirmation. If Shopify answers the scripted POST with a bot challenge (403 or 429), the theme hands the shopper to the native submit, which carries the full browser context and can be verified.
Without scripting
Section titled “Without scripting”The variant picker renders a <noscript> <select name="id" form="pd-form"> listing every variant with its price, with unavailable ones disabled. The form is a real product form, so add-to-cart works without the script — and the theme’s global handler upgrades it to AJAX plus the cart drawer when the script is present.