Live pricing schema
Updated Oct 1, 20266 min read
On this page (8)
Live pricing rewrites the prices your theme already rendered, in the shopper's browser, so a campaign's discounted price shows on product cards and product pages. What it rewrites is up to you: the schema is a JSON array of rules, each pairing a CSS selector with the HTML that should replace what it matches.
Edit it in the Hype admin under Settings → the Live storefront prices card → Edit Selectors. Hype saves it to the hype.pricing_schema app metafield, and the Hype: Discounts app embed inlines it into every storefront page.
Three things must be true before a single price changes: live pricing is on shop-wide, it is on for the campaign, and the schema matches your theme. Campaigns of type Multi Products Discount and Order Discount apply their rules directly; a Volume Discount campaign re-evaluates on every cart change and uses the best tier the cart currently qualifies for.
The rule format
{ "type": "product-card", "template": "all", "parentSelector": ".product-card-wrapper", "selector": ".card .price", "wrapperSelector": "", "replaceContent": "<div class='price'>{{ discountedPrice }}</div>"}| Field | Required | Description |
|---|---|---|
type | Yes | product-card, product-page or text |
template | Yes | all, product or collection. The rule runs only on that kind of page; all runs everywhere. |
parentSelector | product-card only | The repeating card element. Hype derives the product handle from it. |
selector | Yes | The price container to replace |
wrapperSelector | No | Narrows the search when selector matches more than one element |
replaceContent | Yes | The HTML to render in place of the match |
template is checked against the current URL: a path containing /products/ is product, a path starting with /collections/ or /search is collection, and everything else is all. A rule whose template is product never runs on a collection page, and vice versa. Note that a product page reached from a collection URL still counts as product.
The three types behave differently:
product-cardwalks everyparentSelectorelement, reads the product handle from it — an anchor'shref, adata-product-handleattribute, or a descendant link to/products/…— findsselectorinside that card, and replaces it. Cards have no variant, so card prices always use the product's own price.product-pagetakes the handle from the page URL and replaces the first matching element on the page. Add a second rule if your template shows the price twice, for example in a sticky bar.textreplaces every match withreplaceContent, and receives no variables. Use it for fixed copy, not for prices.
Selector resolution order
For each rule, Hype resolves the element to replace in this order:
product-card— collect everyparentSelectorelement that is not already markeddata-hype-processed. Then, inside each card: ifwrapperSelectoris set, find the first wrapper that containsselectorand take the match inside it; otherwise take the firstselectormatch inside the card.product-page— ifwrapperSelectoris set, search the page for wrappers and take the firstselectormatch inside one; otherwise take the firstselectormatch on the page.text— ifwrapperSelectoris set, take everyselectormatch inside every wrapper; otherwise take every match on the page.
Every one of these searches walks open shadow roots as well as the light DOM, so a theme that renders prices inside a web component is still reachable.
Variables
replaceContent is rendered with these values. They resolve on the product's selected variant on a product page, and on the product itself on a card.
| Variable | Value |
|---|---|
{{ price }} | The original price, formatted in the shop's money format |
{{ rawPrice }} | The original price with no currency symbol |
{{ discountedPrice }} | The price after the campaign discount, formatted |
{{ rawDiscountedPrice }} | The same, with no currency symbol |
{{ discountPercentage }} | For a percentage discount, the exact value, e.g. 20%. For an amount discount, the computed equivalent to two decimals. |
{{ discountAmount }} | The money saved, formatted |
{{ compareAtPrice }} | The product's own compare-at price, unformatted. Empty when there is none. |
{{ product.title }} | Product title |
{{ product.handle }} | Product handle |
{{ product.url }} | Product URL |
Spaces inside the braces are optional: {{ price }} and {{price}} are the same variable.
What replaceContent can and cannot do
replaceContent is not Liquid. The renderer only substitutes {{ variable }} and {{ object.path }}.
- Filters do nothing.
{{ price | money }}is not a variable name and renders empty. Use the already-formattedpriceanddiscountedPrice. - Tags do nothing. There is no
{% if %}and no loop; a rule cannot branch on whether the product is on sale. - An unknown variable renders as an empty string, not an error.
- The rendered HTML is applied with a DOM-morphing patch against the matched element, so start
replaceContentwith the same kind of element you matched. Copy your theme's markup exactly — every class, id and data attribute — and swap only the price values. Missing classes lose your theme's styling; missing data attributes can break the theme's own scripts. - Capture the whole price container, including the compare-at and sale markup. Matching only the inner number leaves the theme's original price behind next to yours.
A card is processed once: Hype marks its parentSelector element with data-hype-processed. New cards added by lazy loading are picked up by a mutation observer, and variant changes on a product page re-render through the theme's input events, so the price follows the shopper's selection.
A Dawn example
This is the schema Hype seeds for a new shop, matching Shopify's Dawn theme: one rule for product cards on any page, one for the product page.
[ { "template": "all", "type": "product-card", "parentSelector": ".product-card-wrapper", "selector": ".card > .card__content > .card__information > .card-information > .price ", "replaceContent": "<div class='price price--on-sale'><div class='price__container'><div class='price__regular'><span class='visually-hidden visually-hidden--inline'>Regular price</span><span class='price-item price-item--regular'>{{ price }}</span></div><div class='price__sale'><span class='visually-hidden visually-hidden--inline'>Regular price</span><span><s class='price-item price-item--regular'>{{price}}</s></span><span class='visually-hidden visually-hidden--inline'>Sale price</span><span class='price-item price-item--sale price-item--last'>{{ discountedPrice }}</span></div><small class='unit-price caption hidden'><span class='visually-hidden'>Unit price</span><span class='price-item price-item--last'><span></span><span aria-hidden='true'>/</span><span class='visually-hidden'> per </span><span></span></span></small> </div> </div>" }, { "template": "all", "type": "product-page", "selector": ".product__info-container .price ", "replaceContent": "<div class='price price--on-sale'><div class='price__container'><div class='price__regular'><span class='visually-hidden visually-hidden--inline'>Regular price</span><span class='price-item price-item--regular'> {{price}}</span></div><div class='price__sale'><span class='visually-hidden visually-hidden--inline'>Regular price</span><span><s class='price-item price-item--regular'>{{price}}</s></span><span class='visually-hidden visually-hidden--inline'>Sale price</span><span class='price-item price-item--sale price-item--last'>{{discountedPrice}}</span></div><small class='unit-price caption hidden'><span class='visually-hidden'>Unit price</span><span class='price-item price-item--last'><span></span><span aria-hidden='true'>/</span><span class='visually-hidden'> per </span><span></span></span></small></div></div>" }]Both rules add Dawn's price--on-sale class, which is what makes Dawn strike the original price and color the sale price. A different theme needs its own equivalent class.
Testing a schema
You do not need a live campaign to check your selectors:
- Go to Settings and find the Live storefront prices card.
- Click Test Live pricing.
- Enter a test value between 1 and 100 — it is a percentage, and it starts at 100.
- Click Preview Theme.
Your storefront opens with hype-dynamic-price-test=<value> in the URL, and every product renders as if that percentage discount applied, whatever your campaigns are doing. Anything that does not change is a selector that did not match.
Turn on Enable debug logs in the Hype: Discounts app embed to see which elements and handles the engine resolved in the browser console.
Using the schema alongside SSR live pricing
When a campaign has both live pricing and SSR live pricing on, Hype skips any element carrying data-hype-ssr — the attribute the SSR snippet sets on a price container it discounted — so a snippet on the current version can keep a broad schema. If the snippet predates the attribute, every rule that matches a Liquid-rendered price makes that price repaint after load: point rules only at the areas your theme does not render server-side, give app-rendered cards their own product-card rule with a parentSelector unique to them, and drop the product-page rule for that campaign. See Using SSR live pricing together with live pricing.
Related
Was this article helpful?
Related articles
Still stuck?
Ask a follow-up and we'll see it came from “Live pricing schema”.
Opens chat with your question and a link to this article.