Shared Product Components
What this is
These are the small, reusable building blocks that the product card, the product detail page, and the featured‑product section all share. Because they're shared, a product's price, variant pickers, badges, and stock messages look and behave the same everywhere in the store.
This document explains each shared component in plain language, then collects a few cross‑cutting technical patterns at the end.
The components covered here:
| Component | What it does | File |
|---|---|---|
| Price | Shows price, sale price, "From" price, unit price | `snippets/price.liquid` |
| Variant options | The colour/size pickers (swatch / button / dropdown) | `snippets/product-options.liquid` (+ `option-group.liquid`, `swatch.liquid`, `swatch-input.liquid`) |
| Badges | Sale / Sold out / New / custom labels | `snippets/badge.liquid` |
| Inventory | Stock status messages | `snippets/inventory.liquid` |
| Buy buttons | Add to cart + dynamic checkout | `snippets/buy-buttons.liquid` (+ `product-form.liquid`) |
| Quick add | Add/preview a product without leaving the page | `snippets/quick-add-modal-global.liquid`, `assets/quick-add-modal.js` |
Price
What it does. Displays a product's price in all its forms: regular price, a sale price with the original crossed out, a "From" price when variants cost different amounts, unit pricing (e.g. per 100 ml), and optional bulk/volume price ranges. It can also show small sale / sold‑out badges.
Where it's used. Product cards, the product detail page, and featured product anywhere a price appears.
Good to know.
- It automatically detects a sale (when there's a higher "compare‑at" price) and styles it accordingly.
- "From" pricing appears when a product's variants have different prices.
- Colours can be overridden for light and dark mode where the price is used.
Behind the scenes (for developers):snippets/price.liquidresolves atarget(variant whenuse_variant, else product), computesmoney_price/money_compare/volume range, supportsmoney_with_currencywhencurrency_code_enabled, and shows a "From" price when the product price varies. State classes on the root includeproduct-price--on-sale,--sold-out,--volume-sale,--no-compare,--show-badge; it exposesdata-price,data-compare-at-price,data-available,data-variant-id,data-volume-pricingfor the JS to read on variant change. Sub‑containersjs-price-regular/js-price-sale/js-price-compare(a<del>), plus optionalunit-priceand badges (js-price-badge-sale/-soldout). Scoped colour overrides via{% style %}on.product-price--<id>. Loadsproduct-price.css.
Variant options (swatches, buttons, dropdowns)
What it does. Renders the pickers a shopper uses to choose a variant — for example Color and Size. Each option can be shown as:
- Swatches — colour/image chips,
- Buttons — labelled buttons,
- Dropdown — a select menu.
When a shopper picks a value, the rest of the page (price, image, availability, add button) updates to match.
Two contexts. The same component is used in two places, with slightly different behaviour:
- On a product card — compact; may show only some options, or none (in which case the card uses a quick‑view popup).
- On the product page — full pickers with labels and colour styling.
Good to know.
- The theme picks a sensible picker type automatically, but you can force swatch/button/dropdown via settings.
- Swatches show an "unavailable" look for variant values that can't be bought.
Note
Behind the scenes (for developers): snippets/product-options.liquid resolves each option to swatch / button / dropdown / swatch_dropdown using explicit label maps (product_options_button_labels, …_dropdown_labels) or swatch‑count heuristics (falling back to button when a swatch picker has no swatches). Card context honours product_options_enable / product_options_visible_labels. Each option is delegated to snippets/option-group.liquid, which renders a <fieldset data-variant-selector-input> (swatch/button) or <custom-dropdown data-variant-selector-form>; values carry data-product-url + data-option-value-id. assets/product-options.js collects the selected data-option-value-ids and fires the `product-options:change` event (this is what the product card and <product-info> listen for). Swatches come from swatch-input.liquid (hidden radio + label) → swatch.liquid (--swatch--background colour/image + focal point). Loads product-options.css and swatch.css; enqueues product-options.js (and custom-dropdown.js when dropdowns are present).
Badges (Sale / Sold out / New / custom)
What it does. Shows small labels over a product image:
- Sale — optionally as a percentage or amount saved,
- Sold out,
- New — for recently published products (within a configurable number of days),
- Custom labels — driven by product tags that start with
label__.
Good to know.
- Badges can be turned off globally, or per place via a "show badges" option.
- Position, shape, and colours are configurable in theme settings.
Behind the scenes (for developers):snippets/badge.liquidexits early if there's no product,show_badges == false, orsettings.badges_enable == false. It computes sold‑out / on‑sale / savings, reads position & radius from settings, emits scoped colour vars via<style>, and renders sold‑out, new (withinbadges_new_daysofpublished_at), sale (text/percent/amount), and custom badges from tags prefixedlabel__.
Inventory (stock status)
What it does. Shows a short stock message such as "In stock", "Low stock", or a quantity, often with a small coloured dot. It updates when the shopper changes variant.
Good to know.
- Only shows when inventory is tracked by Shopify and the feature is enabled.
- The thresholds for "low" vs "very low" stock are configurable.
Behind the scenes (for developers):snippets/inventory.liquidskips card context unlessinventory_enableandinventory_management == 'shopify'. It mapsinventory_quantityto astock_level(high/medium/low/very_low/out/continue) against configurable thresholds, then to a colour var + localized message. Product context usesid="Inventory-<section_id>"+js-inventory; card context usesjs-card-inventory. Loadsinventory.css.
Buy buttons (Add to cart + checkout)
What it does. The Add to cart button plus optional dynamic checkout buttons (e.g. Shop Pay / "Buy it now"). Supports extras like pre‑order labels, showing the price inside the button, and a gift‑card recipient form.
How adding to cart works. The button lives inside a product form. On the product page and on cards, adding happens without a full page reload (the item is added in the background and the cart updates).
Behind the scenes (for developers):snippets/buy-buttons.liquidrenders the add‑to‑cart control and dynamic checkout; the underlying form issnippets/product-form.liquidinside a<product-form>custom element (submit logic inassets/product-form.js). The submit control exposesjs-product-form-submit/js-product-form-submit-textand a hiddenjs-product-form-variant-idthe same hooks the product card updates on variant change. Cards render this viacard-add-button.liquidwithcontext: 'card'; the PDP renders it as the Buy buttons block.
Quick add / Quick view
What it does. Lets a shopper add a product to the cart (or preview it) without leaving the page they're on. From a product card, clicking the quick‑add button opens a popup containing the product's details so they can pick a variant and add to cart.
Good to know.
- The popup is a single shared element placed once on every page.
- It fetches the real product content, so what the shopper sees matches the product page.
- There's also a B2B/wholesale "quick‑order" mode for bulk variant ordering.
Note
Behind the scenes (for developers): the shell snippets/quick-add-modal-global.liquid is rendered once in layout/theme.liquid as <quick-add-modal id="QuickAddGlobal"> (overlay, dialog, skeleton loader, js-quick-add-content). assets/quick-add-modal.js delegates clicks on [js-quick-add-trigger], calls show(trigger), and fetchProductContent(productUrl, mode) does a fetch of the product URL and extracts product-info (default) or <quick-order-list> (B2B mode). preprocessContent de‑dupes element IDs (quickadd-<sectionId>), strips the sticky bar/modal, fixes image sizes, and disables variant‑URL switching, then injects, re‑inits payment buttons, dispatches richtheme:dom:rescan, and traps focus. Note this is a full‑product fetch, distinct from the lighter card variant fetch (see Product Cards).
Cross‑cutting technical patterns (for developers)
These conventions apply across all the product components.
1. Source files compile to served files
| You edit (source) | The build produces (served) |
|---|---|
| `src/styles/scss/**/*.scss` | `assets/*.css` |
| `src/scripts/**/*.ts` | `assets/*.js` |
Warning
️ Edit the source, never the compiled assets/*.css / assets/*.js — they're regenerated by the build and hand edits are lost. Each compiled file's header comment points to its source.
2. Light/dark colour pattern
Colour custom properties are emitted in inline <style> blocks split by theme:
- Light mode:
html:not(.dark-theme) body:not(.dark-theme) <scope> { … } - Dark mode:
.dark-theme <scope> { … }
Each colour is only output when the merchant actually set one (the Liquid checks the value contains '#'), so unset colours fall back to the global theme colours. Scopes are usually a unique per‑section/per‑block class (e.g. .main-product-section-id-<id>, .product-price--<id>).
3. Live updates via the Section Rendering API
When a shopper changes a variant:
assets/product-options.jsfires `product-options:change`.- Listeners (
<product-info>on the PDP,<product-card>on cards) fetch fresh HTML from Shopify's Section Rendering API (e.g.…?section_id=product-card-ajax&option_values=…) and surgically swap in the new price, image, stock, and button state — no full page reload.
4. Shared data hooks
Components expose predictable js-… classes and data-… attributes (e.g. js-price-regular, js-product-form-variant-id, data-option-value-id, data-variant-media-id) so the JavaScript can read selections and update the DOM in place. Keep these hooks intact when customizing markup.
Related docs
- [Product Cards](./Product%20Cards.md) — where these components appear in the compact tile.
- [Main Product Section](./Main%20Product%20Section.md) — the full product page that arranges these as blocks.
- [Featured Product](./Featured%20Product.md) — the portable single‑product section.