Skip to content

Volume discounts

The PDP volume-discount widget ("Buy more, save more" - quantity breaks) renders the one live

The PDP volume-discount widget (“Buy more, save more” - quantity breaks) renders the one live discount the Storesynk Shopify app synced for the current product: a PUBLIC_READ $app:volume_discount metaobject (title, status, tiers, targets, date window), discovered through the product’s volume_discounts metafield plus the shop-level list (for all / collections-targeted discounts, membership-checked against the product’s own collections), read with the browser-safe public token. Only an active discount inside its date window qualifies, and the first in list order wins. It rides the same on-demand bundle.js chunk as the bundle widget (loaded when a <storesynk-volume-discount> is on the page - nothing to configure). Blueprint: build-patterns §2.11.

Namespace is required configuration (identical to bundles): the app’s concrete namespace (app--<numeric-app-id>) must reach the widget via app-namespace on <storesynk-store> (recommended) or a per-widget namespace attribute - neither → the widget stays [ss-empty] (SSK-211).

All prices are advisory - computed client-side from the current selected variant’s price (rows re-price on every sf:variant-change); the app’s Discount Function applies the real percentage at checkout. <add-volume-to-cart> adds exactly the selected tier’s quantity of the current variant (an allowExtraItems “N+” tier still adds N - the customer edits the quantity in the cart).

  • Purpose: the widget root. Queries the current product’s live volume discount, clones its template once for the winner, and provides volumeDiscountContext (entry, resolved tiers, selectedIndex).
  • Attributes:
    • namespace : string = "" - overrides the store-level app-namespace for this widget alone.
  • Required ancestor / context: storesynk-product (consumes productContext for the product id - SSK-102) + storeContext (client, locale, appNamespace).
  • Children / template rule: TEMPLATE-CLONE - its first element child is the per-discount template (cloned once); it must contain a volume-tier-list (SSK-107).
  • Events / state: catches sf:tier-select from its rows (records the selection + re-provides); re-prices its tiers on sf:variant-change. Sets [ss-loading] during the fetch and [ss-empty] when there is no live discount (or no namespace) - hide it via storesynk-volume-discount[ss-empty] { display: none }.
  • Purpose: the discount’s tier rows. Clones its row template once per tier and feeds each clone a per-row volumeTierContext; it is the ARIA radiogroup (role="radiogroup" + an aria-label from the discount title unless you label it) and owns the APG keyboard pattern (Arrow/Home/End move focus and select, Space selects the focused row).
  • Attributes: none.
  • Required ancestor / context: storesynk-volume-discount (consumes volumeDiscountContext - SSK-105).
  • Children / template rule: TEMPLATE-CLONE - its first element child (a volume-tier, or a wrapper containing one) is the tier-row template (SSK-107). Selection/price updates refresh rows in place (no re-clone).
  • Purpose: one selectable tier row - the radio control itself (role="radio", roving tabindex, aria-checked, ss-active all track the selection; its accessible name comes from its show-tier-* contents).
  • Attributes: none. Click / keyboard selection dispatches sf:tier-select (detail = { index }) up to the widget root.
  • Required ancestor: volume-tier-list (SSK-106 - only the list clones and drives it).
  • Purpose: the widget heading - the metaobject’s customer-facing title (e.g. “Buy more, Save more”).
  • Attributes: none.
  • Required ancestor / context: storesynk-volume-discount (consumes volumeDiscountContext - SSK-105).

show-tier-title / show-tier-label / show-tier-badge / show-tier-price / show-tier-original-price / show-tier-each-price

Section titled “show-tier-title / show-tier-label / show-tier-badge / show-tier-price / show-tier-original-price / show-tier-each-price”
  • Purpose: per-tier-row displayers - the resolved title (“Buy 2”); the label (“Save 10%”); the badge (“Most popular”); the tier’s discounted total; the undiscounted total for a struck compare-at; and the discounted per-item (“X each”) price. Money displayers format via the store’s formatMoney.
  • Attributes: none.
  • Required ancestor / context: inside the tier-row template within volume-tier-list (consume the per-clone volumeTierContext - SSK-105); + storeContext for money.
  • State: show-tier-label / show-tier-badge hide (native hidden + [ss-empty]) when the tier has no label/badge; show-tier-original-price hides the same way on rows with no savings. Presentation is template composition: include show-tier-original-price for struck compare-at prices (the reference layout does); include show-tier-each-price for per-item prices (the reference default omits it).
  • Purpose: adds the selected tier’s quantity of the current variant to the cart via the same store.addToCart path as add-to-cart (so the standard shopify:cart:* events fire and the drawer’s open-on-add works). Extends VariantButton.
  • Attributes: text-out-of-stock, text-unavailable (optional label swaps, like add-to-cart). Recommended: nest a native <button> (else the host upgrades itself to role="button" + tabindex + Enter/Space).
  • Required ancestor / context: storesynk-volume-discount (consumes volumeDiscountContext for the selected tier - SSK-105) + productContext + storeContext.
  • Events: emits sf:add-to-cart (bubbles, composed) on a successful add.
  • State it sets (VariantButton conventions): [ss-out-of-stock] / [ss-unavailable] tracking the page’s selected variant, [ss-loading] during the add, aria-disabled="true" on the focusable control when blocked.

Imported from the Storesynk skill v0.1.0 (references/component-reference.md). To change this page, change it there.