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).
storesynk-volume-discount
Section titled “storesynk-volume-discount”- Purpose: the widget root. Queries the current product’s live volume discount, clones its
template once for the winner, and provides
volumeDiscountContext(entry, resolvedtiers,selectedIndex). - Attributes:
namespace : string = ""- overrides the store-levelapp-namespacefor this widget alone.
- Required ancestor / context:
storesynk-product(consumesproductContextfor 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-selectfrom its rows (records the selection + re-provides); re-prices its tiers onsf:variant-change. Sets[ss-loading]during the fetch and[ss-empty]when there is no live discount (or no namespace) - hide it viastoresynk-volume-discount[ss-empty] { display: none }.
volume-tier-list
Section titled “volume-tier-list”- 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"+ anaria-labelfrom 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(consumesvolumeDiscountContext- 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).
volume-tier
Section titled “volume-tier”- Purpose: one selectable tier row - the radio control itself (
role="radio", rovingtabindex,aria-checked,ss-activeall track the selection; its accessible name comes from itsshow-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).
show-volume-discount-title
Section titled “show-volume-discount-title”- Purpose: the widget heading - the metaobject’s customer-facing title (e.g. “Buy more, Save more”).
- Attributes: none.
- Required ancestor / context:
storesynk-volume-discount(consumesvolumeDiscountContext- 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-clonevolumeTierContext- SSK-105); +storeContextfor money. - State:
show-tier-label/show-tier-badgehide (nativehidden+[ss-empty]) when the tier has no label/badge;show-tier-original-pricehides the same way on rows with no savings. Presentation is template composition: includeshow-tier-original-pricefor struck compare-at prices (the reference layout does); includeshow-tier-each-pricefor per-item prices (the reference default omits it).
add-volume-to-cart
Section titled “add-volume-to-cart”- Purpose: adds the selected tier’s quantity of the current variant to the cart via the
same
store.addToCartpath asadd-to-cart(so the standardshopify:cart:*events fire and the drawer’sopen-on-addworks). Extends VariantButton. - Attributes:
text-out-of-stock,text-unavailable(optional label swaps, likeadd-to-cart). Recommended: nest a native<button>(else the host upgrades itself torole="button"+tabindex+ Enter/Space). - Required ancestor / context:
storesynk-volume-discount(consumesvolumeDiscountContextfor 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.