# discountkit — full documentation > The discounting platform for Shopify. This file concatenates every documentation page; canonical URLs are noted per section. --- # Discount Kit documentation Source: https://discountkit.app/docs/ Discount Kit runs every promotion you can imagine: pick a discount type, set the rules, and it runs everywhere — cart, checkout, and your storefront. Every discount type is a Shopify discount function, so it works with your theme out of the box and runs on Shopify's own infrastructure. Think of Discount Kit as extending Shopify's native discounts. ## Discounts Each discount type is a Shopify discount you create from your store's **Discounts** page. title="Custom" description="Your own rules and conditions, from simple to fully custom." href="/docs/discounts/custom/" /> title="Product volume" description="Tiered discounts on a product by quantity or spend." href="/docs/discounts/product-volume/" /> title="Tiered rewards" description="A spend-and-save ladder of order, shipping and gift rewards." href="/docs/discounts/tiered-rewards/" /> title="Gift with purchase" description="Free gifts customers unlock and choose." href="/docs/discounts/gift-with-purchase/" /> title="Shipping" description="Shipping deals by rate name, price, cart value, quantity or weight." href="/docs/discounts/shipping/" /> title="Sale" description="Different markdowns for several collections, in one discount." href="/docs/discounts/sale/" /> ## Components Ready-made storefront components that show your discounts: prices, volume pickers, sale badges, tiered rewards bars and a product quickview. title="Components overview" description="Enable the Components embed and place your first component." href="/docs/components/overview/" /> title="Components or Live data?" description="When to use a ready-made component, and when to build your own." href="/docs/live/components-or-live-data/" /> ## Live data Your discount data, published straight to storefront metafields for your own Liquid — no API calls, no performance overhead. title="Introduction" description="What syncs by default, the dk:live tag, limits and fan-out." href="/docs/live/introduction/" /> title="Storefront data" description="Reading discounts from product, collection and shop metafields." href="/docs/live/storefront-data/" /> ## Help title="Test mode" description="Try a discount on your own store before customers see it." href="/docs/test-mode/" /> title="Percentage discount rounding" description="Why percentage discounts on low unit prices don't always match the expected total." href="/docs/general/percentage-discount-rounding/" /> --- # AI tools (MCP) Source: https://discountkit.app/docs/ai-tools/ The discountkit documentation is available as a remote [MCP](https://modelcontextprotocol.io) server, so your AI editor can search and read these docs directly. No install, no API key — it runs on the same edge deployment as this site. **Endpoint:** `https://discountkit.app/mcp` (streamable HTTP) ## Setup ```bash claude mcp add --transport http discountkit-docs https://discountkit.app/mcp ``` Add to `.cursor/mcp.json`: ```json { "mcpServers": { "discountkit-docs": { "url": "https://discountkit.app/mcp" } } } ``` Any MCP client with streamable HTTP support can connect to the endpoint directly. For stdio-only clients, bridge with: ```bash npx -y mcp-remote https://discountkit.app/mcp ``` ## Tools | Tool | What it does | | --- | --- | | `search_docs` | Ranked search across every docs page, with snippets and URLs | | `get_doc` | One page as full markdown, by path | | `list_docs` | Every page with title, description and URL | The machine-readable catalog lives at [`/mcp-schema.json`](/mcp-schema.json). Prefer plain files? The whole documentation set is also published at [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt). --- # App blocks vs. Web Components Source: https://discountkit.app/docs/components/app-blocks-vs-web-components/ Every component can be placed two ways. They render the **same component** from the **same data** — the difference is who places it, how it's configured, and where it's allowed to go. (The one exception is the [Quickview](/docs/components/quickview/overview/), which is web-component and JavaScript only — its trigger is your own markup, and it fetches its product itself, so it works on any page.) - **App blocks** — added through the **theme editor**. No code; you pick a component you built in the admin. The right choice for most merchants, and the only way to render inside **product cards** on collection pages. - **Web Components** — the `` element written **directly into your theme's Liquid**. Configured with `data-*` attributes and `--dkl-*` CSS. For developers customizing a theme by hand. (These are labelled "Web Components" on each component page.) ## At a glance | | App block | Web Component | | --- | --- | --- | | **Added by** | Theme editor (drag-and-drop) | Hand-written in theme Liquid/HTML | | **Configuration** | A component, built in the admin | `data-*` attributes, or a component by handle | | **Styling** | The component's editor, with a live preview | `--dkl-*` CSS styling tokens | | **Which discount** | Carried by the component | A component by handle (`data-component`), or the `data-discount-id` attribute | | **Product cards / collection grids** | ✅ Supported | ⚠️ Partial support — varies per component | | **Needs the app embed** | ✅ Yes | ✅ Yes | | **No-flicker render** | ✅ Inline server render | ✅ From a head carrier | | **Best for** | Merchants, no code | Developers, full control | :::note Both paths require the **Components** app embed to be enabled — it loads the shared runtime (styles + behaviour) and pre-renders the data each component reads. See [Enable the Components app embed](/docs/components/overview/#enable-the-components-app-embed). ::: ## App blocks There is one app block for every component: **Component**. Its only setting is which component to render — a component you created and styled in the Shopify admin under **Discount Kit → Components**. See [Components & the app block](/docs/components/component-entries/) for that flow. ```text Online Store → Themes → Customize → (add block) → Component → pick your component ``` What an app block does for you: - **Renders whichever component you picked** — the component knows its own type, so the same block places a price, a picker, a table, a goal bar or a badge. - **Resolves the eligible discount itself** — the component carries the discount it was built for, and the block checks eligibility with the shared rules, so it agrees with the web-component path. - **Carries all the styling** — colours, spacing, typography, text and behaviour come from the component, written into the element's `style` attribute. You style it against a live preview in the admin rather than blind in the editor panel. - **Only loads what it needs** — the chosen component's controller and nothing else. - **Reaches contexts a tag can't** — most importantly **product cards** in a collection grid, where there's no single product in `` context to pre-stage a carrier from. Use an app block when a merchant should be able to place the component without touching code, or whenever you need it inside a product card. ## Web Components A web component is the `` web component written straight into your theme's markup: ```html ``` By default, a web component needs **zero attributes** — the page's primary product is the implicit default, and the tag clones a pre-staged carrier (markup, styling, and `data-*` values) before first paint. Configure it with `data-*` attributes and style it with `--dkl-*` CSS: ```html ``` What to know about web components: - **Partial support off product pages.** Product widgets (price, volume picker, volume table, sale badge) have carriers only where there's a single product in context. A collection grid is paginated and per-product, so a hand-typed tag can't be pre-staged there — use the app block for product cards. Tiered rewards belong to the whole cart, so its carriers are on every page: a tag works in a cart drawer, on the cart page or anywhere else. - **Target a specific discount** with a component (`data-component=""`, which carries its discount) or with `data-discount-id` (the metaobject handle; the `discount-` prefix is optional). Place one tag per discount to show several. - **Style with `--dkl-*` tokens** in your own CSS, or **name a component** with `data-component=""` to reuse one you built in the admin. Without it the tag renders the built-in look; set the tokens on `:root` in your theme's CSS, an inline `style`, or any ancestor. The defaults match the app block exactly, so an unstyled tag looks identical to an unstyled component. - **Behaviour toggles may default off.** For the volume picker, native-price update and quantity sync only run if you opt in (`data-update-price-on-change="true"` / `data-sync-with-quantity="true"`); any attribute you set wins over the carrier's. Use a web component when you're editing the theme directly and want precise control over placement, attributes, and CSS. ## Which should I use? The merchant should place it without code · you need it in a **product card** · you want styling managed in the admin, with a live preview. You're editing theme Liquid directly · you want exact control over placement and `data-*` · you're styling with `--dkl-*` CSS · you need several discounts placed by hand. You can still point it at a component with `data-component`. ## Next steps --- # Components & the app block Source: https://discountkit.app/docs/components/component-entries/ A **component** is a saved, styled instance of one of the storefront components. You create it in the Shopify admin under **Discount Kit → Components**, customise it against a live preview, and then place it on your storefront — either with the **Component** theme block or by naming it from a web component. There is one theme block for every component type. What it renders is decided by the component you pick, not by the block, so a new component type never means a new block. ## Create a component 1. **Open Discount Kit → Components** Choose **Create component**, then whether it belongs to a discount or not. 2. **Discount component, or static** A **discount component** is styled for one of your discounts — a volume picker, a volume table, a tiered rewards bar, or a [sale badge](/docs/components/sale-badge/overview/). You pick the discount, then the component and its layout. A **static component** is not tied to a discount: the [price](/docs/components/price/overview/) shows whatever discount applies to the product. 3. **Customise it** Colours, spacing, typography, text and behaviour, all against a live preview of the real component. Desktop and mobile sizes are separate, and the preview switches between them. The preview's **Data** controls stand in for what the storefront would supply — a cart total for tiered rewards, the selected tier for a volume component, the kind of sale (percentage, amount off or fixed price) for a badge — so you can see every state the component has without leaving the editor. 4. **Save** Give it a name — that's what you'll see in the theme editor's picker. ## Place it with the app block 1. **Open the theme editor** **Online Store → Themes → Customize**. Make sure the **Components** app embed is enabled. 2. **Add the Component block** In the section where you want it, click **Add block** and choose **Component**. 3. **Pick your component** Set the block's **Component** setting to the one you created, and position the block. :::note A block with no component selected renders **nothing**. That's deliberate: the block can't know which component you meant, and guessing would put the wrong thing on your storefront. ::: The block isn't restricted to product templates, because the [badge](/docs/components/sale-badge/overview/) has to reach collection **product cards**. A component that needs a product or a discount it can't find in that position simply renders nothing. ## Place it from your theme code A [web component](/docs/components/app-blocks-vs-web-components/) can name a component too, with `data-component` and the component's handle: ```html ``` Copy the handle from the component's **Add to storefront** panel in the admin. Without `data-component` the tag still renders — with the built-in styling, which you can override from your own CSS using the [styling tokens](/docs/components/global-styling/). ## Styles Brand-level values — accent colour, text colour, corner radius, font family — live in one place, **Components → Styles**, and every component falls back to them. Set those once and individual components only need what differs. See [Styling](/docs/components/global-styling/). ## Next steps --- # Events Source: https://discountkit.app/docs/components/gift-selector/events/ The gift selector emits the [quickview's events](/docs/components/quickview/events/) — opened, loaded, panel changes, variant changes, closed — plus the two below. All bubble to `document`; the payload is on `event.detail.resource`. #### `discount-kit-live:quickview:selection-change` `CustomEvent` A slot was filled or cleared. ```js { slots: [ { id: 't1-a', group: 0, variantId: 41200000000001 }, { id: 't1-b', group: 0, variantId: null }, ], complete: false, // true once every slot is filled } ``` #### `discount-kit-live:quickview:add-to-cart` `CustomEvent` In selection mode the add-to-cart event carries `mode: 'selection'` and one entry per written slot in `lines`; the single-line fields describe the first line, for listeners that don't know about selection mode. ```js { mode: 'selection', lines: [ { slotId: 't1-a', productHandle: 'tote-bag', productId: 7820000000001, variantId: 41200000000001, quantity: 1, properties: { _dk_gift: 'Spend $50 gifts', _dk_gift_tier: '0' }, variant: { … }, featuredImage: 'https://cdn.shopify.com/…', selectedOptionValues: ['Natural'], }, ], productHandle: 'tote-bag', // …the first line, repeated productId: 7820000000001, variantId: 41200000000001, quantity: 1, response: { … }, // the cart write's result, or null with submitLines } ``` Product-mode opens are unchanged and carry `mode: 'product'` with a one-entry `lines`. #### `discount_kit:gift_choices` `CustomEvent` on `document`, from the Gift with purchase embed Fired after every cart pass with `detail.changeable`: whether a claimed gift tier offers a choice. [``](/docs/components/gift-selector/web-component/) shows and hides on it. #### `discount_kit:gift_adjustments` The embed's own event listing gifts to add and remove on every cart pass, documented on the [Gift with purchase](/docs/discounts/gift-with-purchase/#custom-integration) page. --- # JavaScript API Source: https://discountkit.app/docs/components/gift-selector/javascript-api/ The gift selector is the [quickview](/docs/components/quickview/javascript-api/) opened with `mode: 'selection'`. The Gift with purchase embed builds that request for you; this page is for custom gift experiences that open it themselves — everything here is the same `DiscountKitLive.openQuickview()` helper with a few more fields, and everything on the quickview page (the promise, the command events, `closeQuickview()`, labels) applies. ## Which API do you need? - **Just the embed's behaviour, with your own button** — place [``](/docs/components/gift-selector/web-component/) or call `window.discount_kit.core.openChangeGifts()`. No request to build. - **Your own gift logic** (a custom app, a rule the discount can't express, a loyalty reward) — open selection mode yourself with `groups` and `slots`, and either let the dialog write to the cart or take the lines with `submitLines`. - **Reacting to what the embed does** — listen to `discount_kit:gift_adjustments` (what the embed wants to add and remove), `discount-kit-live:quickview:selection-change` (what the shopper has picked) and `discount-kit-live:quickview:add-to-cart` (what was written). See [Events](/docs/components/gift-selector/events/). ## Selection mode ```js await window.DiscountKitLive.openQuickview({ mode: 'selection', groups: [ { tier: 1, choices: [{ productHandle: 'tote-bag' }, { productHandle: 'beanie' }], lineProperties: { _dk_gift: 'Spend $50 gifts', _dk_gift_tier: '0' }, }, { tier: 2, choices: [{ productHandle: 'water-bottle', variantIds: [41200000000001, 41200000000002] }], lineProperties: { _dk_gift: 'Spend $50 gifts', _dk_gift_tier: '1' }, }, ], slots: [ { group: 0, id: 't1-a' }, { group: 0, id: 't1-b' }, { group: 1, id: 't2-a', label: 'Bottle' }, ], pickerHeading: "You've unlocked 3 free gifts!", addAllLabel: 'Add gifts to cart', closeOnAdd: true, }) ``` ### `groups` One per gift tier. Each group is the set of products a slot can be filled from. | Field | Type | Purpose | | --- | --- | --- | | `choices` | `{ productHandle \| productId, variantIds? }[]` | The products. `variantIds` narrows a product to the variants the tier permits: values no permitted variant carries are hidden in the option picker, and the action refuses anything else. Absent = every variant. | | `tier` | `number` | Captions the group "Tier [n]" in the tray (captions show with two or more groups). | | `heading` | `string` | A caption of your own instead; also the picker heading while the group is active when no `pickerHeading` was given. | | `lineProperties` | `Record` | Line-item properties on every line filled from this group, merged over the request's own. | A group with **one** product opens straight on that product with no picker and no back button. ### `slots` One per gift, in display order. | Field | Type | Purpose | | --- | --- | --- | | `group` | `number` | Index into `groups`. | | `id` | `string` | Echoed back on the add event as `slotId`. | | `label` | `string` | Text under the empty tray cell (default "Gift [n]"). | | `variantId` | `number` | Pre-fill with a variant of one of the group's products — a gift already in the cart. A tier whose slots all arrive pre-filled opens on its first slot with the action reading "Change gift 1 of N". | | `quantity` | `number` | Units the slot adds (default 1). | ### Result The promise resolves like any other open: `{ panel: 'product' | 'products', productHandle, productId }` — `product` when the active tier has one choice and the dialog opened straight on it, `products` when it opened on the picker. It rejects if the bundle fails to load or the dialog is closed before the first product renders. ### Copy on the request `pickerHeading` (the dialog's heading on the picker), `selectionHeading` (the tray heading with one group), `addToSelectionLabel` (the product screen's action) and `addAllLabel` (the add-all button). Everything else is a [label](#labels). ### Owning the write: `submitLines` By default the add-all action writes the filled slots to the cart itself. Pass `submitLines` to take the lines instead — the way the embed replaces gifts already in the cart: ```js await window.DiscountKitLive.openQuickview({ mode: 'selection', groups, slots, submitLines: async (lines) => { // lines: [{ variantId, quantity, properties }] const ok = await myCart.replaceGifts(lines) return ok ? { ok: true } : { ok: false, message: 'Could not update your gifts' } }, }) ``` Resolve `{ ok: true }` once the cart holds the lines — the dialog then emits `discount-kit-live:quickview:add-to-cart` with the lines as usual — or `{ ok: false, message }` to show the message and keep the dialog open. ### Behaviour in selection mode Tiers are claimed whole: the add-all button is enabled only when at least one tier is complete and none is half-picked, and it writes the complete tiers, leaving an untouched one open for later. The product screen's action fills the tray cell ringed in the accent (the cell tapped, else the active tier's first empty one, else — for a tier that opened pre-filled — its first slot, so a replacement can be picked without clearing anything), reads **Add gift n of N** on an empty cell and **Change gift n of N** on a filled one, and is disabled while the selected options are the variant that cell already holds. Quantity controls and the in-dialog volume picker are off — a gift is not a volume purchase. ## Example Use Cases **A loyalty reward the discount engine doesn't know about.** Your app decides the shopper may pick one of three products; the dialog handles the picking and the cart write and tags the line so your backend can recognise it: ```js const { panel } = await window.DiscountKitLive.openQuickview({ mode: 'selection', groups: [{ choices: [{ productHandle: 'tote-bag' }, { productHandle: 'beanie' }, { productHandle: 'socks' }], lineProperties: { _loyalty_reward: 'silver-tier' }, }], slots: [{ group: 0, id: 'reward' }], pickerHeading: 'Pick your silver-tier reward', addAllLabel: 'Claim reward', closeOnAdd: true, }) ``` **Pick a colour, not a product.** One product with a variant allow-list opens straight on that product with only the permitted colours showing: ```js window.DiscountKitLive.openQuickview({ mode: 'selection', groups: [{ choices: [{ productHandle: 'water-bottle', variantIds: allowedVariantIds }] }], slots: [{ group: 0, id: 'bottle', label: 'Your bottle' }], optionsHeading: 'Choose a colour', closeOnAdd: true, }) ``` **Change what's already there.** Pre-fill the slots from the cart's lines and take the write yourself, so old lines are swapped for new ones rather than added to: ```js const current = cart.items.filter((line) => line.properties?._loyalty_reward) await window.DiscountKitLive.openQuickview({ mode: 'selection', groups: [{ choices, lineProperties: { _loyalty_reward: 'silver-tier' } }], slots: current.map((line, i) => ({ group: 0, id: `reward-${i}`, variantId: line.variant_id })), closeOnAdd: true, submitLines: async (lines) => { // remove every current reward line, add the chosen ones — one request via /cart/update.js // then /cart/add.js, or one updateCart call where the theme exposes standard actions await myCart.replace(current, lines) return { ok: true } }, }) ``` The dialog opens on the first slot reading **Change gift 1 of N**, disabled until the shopper picks something different. **Show the picks somewhere else.** Mirror the tray in your own UI — a cart drawer header, a progress step — from the selection event: ```js document.addEventListener('discount-kit-live:quickview:selection-change', (e) => { const { slots, complete } = e.detail.resource drawerHeader.textContent = `${slots.filter((s) => s.variantId).length} of ${slots.length} gifts chosen` drawerHeader.classList.toggle('is-complete', complete) }) ``` **Drive the embed from your own cart UI.** When the embed owns the gifts, don't rebuild the request — ask it: ```js document.addEventListener('discount_kit:gift_choices', (e) => { myDrawer.querySelector('.change-gifts').hidden = !e.detail.changeable }) myDrawer.querySelector('.change-gifts').addEventListener('click', () => { window.discount_kit.core.openChangeGifts() }) ``` (That's exactly what [``](/docs/components/gift-selector/web-component/) does, so wrapping your button in it is shorter.) **A fully custom gift flow on the embed's data.** The embed publishes what it would add and remove on every cart pass; with the selector turned off in its settings, you can open the dialog yourself from that: ```js document.addEventListener('discount_kit:gift_adjustments', (e) => { const { add } = e.detail if (!add.length) return window.DiscountKitLive.openQuickview({ mode: 'selection', groups: add.map((tier) => ({ tier: tier.discountTier + 1, choices: tier.options.map((o) => ({ productHandle: o.handle, // only a rule that names variants restricts the picker; `anyVariant` tiers stay open ...(o.anyVariant ? {} : { variantIds: o.variants.map((v) => v.id) }), })), lineProperties: { _dk_gift: tier.giftMarker, _dk_gift_tier: String(tier.discountTier) }, })), slots: add.flatMap((tier, group) => Array.from({ length: tier.quantity }, (_, i) => ({ group, id: `${tier.discountId}-${tier.discountTier}-${i}` })), ), closeOnAdd: true, }) }) ``` Keep the `_dk_gift` / `_dk_gift_tier` properties: they are how the discount function and the embed recognise a gift line. Take `_dk_gift` from each add's `giftMarker`: a gift with purchase marks its gifts with its title, but a [tiered rewards](/docs/discounts/tiered-rewards/) discount marks them with its own id, and its function only discounts lines carrying it. ## The change-gifts decision When the embed drives the gift selector, these are on `window.discount_kit.core`: - `canChangeGifts()` — `true` while a claimed tier offers a choice. - `openChangeGifts()` — reopen the selector on the claimed tiers, pre-filled. `false` when there is nothing to change. - `changeableGifts()` — the underlying list: `[{ item, currentLines }]` per changeable tier. `discount_kit:gift_choices` fires on `document` with `detail.changeable` after every cart pass; [``](/docs/components/gift-selector/web-component/) follows it. ## Labels Override any subset globally through `window.DklContext.quickviewLabels`, exactly as for the [quickview's labels](/docs/components/quickview/javascript-api/#labels--translations): | Key | Default | Where | | --- | --- | --- | | `selectionHeading` | `Your selection` | Tray heading with one group | | `groupSelectionHeading` | `[group] selection` | Tray heading with several groups; `[group]` is the active tier's caption | | `tierLabel` | `Tier [n]` | A group's caption, from its `tier` | | `selectionCount` | `[filled] of [total] chosen` | Tray count | | `chooseMore` / `chooseOneMore` | `Choose [n] more` / `Choose 1 more` | Announced to assistive tech after a pick while the tier has empty slots | | `addToSelection` | `Add to selection` | Product screen action, single-slot tier | | `addToSelectionNth` | `Add gift [n] of [total]` | Product screen action, several slots | | `changeSelection` / `changeSelectionNth` | `Change selection` / `Change gift [n] of [total]` | The same action when the slot already holds a gift | | `addAll` / `addedAll` | `Add to cart` / `Added to cart` | The add-all button | | `slotLabel` | `Gift [n]` | An empty tray cell | | `chooseSlot` / `removeSelection` | `Choose` / `Remove` | Accessible names of a tray cell and its remove button | --- # Gift Selector Source: https://discountkit.app/docs/components/gift-selector/overview/ The **Gift Selector** is how shoppers claim the gifts a [gift-with-purchase discount](/docs/discounts/gift-with-purchase/) unlocks — and the gift offers of a [tiered rewards](/docs/discounts/tiered-rewards/) discount, which run through exactly the same flow. When the cart crosses a tier, a dialog opens with one **slot** per gift, the tier's products to choose from, a **selection tray** that shows what has been picked so far, and a single **Add to cart** that writes every filled slot at once. Once the gifts are in the cart, a **Change gifts** trigger lets the shopper swap them. It is built on the [Quickview](/docs/components/quickview/overview/) — the same dialog, opened in its *selection mode* — so everything the quickview does (option pickers, swatches, variant availability, the Storefront and Ajax product sources, brand tokens, CSS parts) applies here too. The two are documented separately because they are used differently: the quickview is a trigger you place on a product; the gift selector opens itself from the Gift with purchase app embed, and the only element you place for it is the change-gifts trigger in the cart. ## How it opens The **Gift with purchase** app embed is the orchestrator: it watches the cart, works out which tiers are unlocked, adds single-option gifts by itself, removes gifts whose trigger lapsed, and — with **Use the Discount Kit quickview (beta)** on and the Components embed enabled — opens the gift selector for anything with a choice. Without the Components embed, or with the setting off, the classic gift selector modal opens as before. Each unlocked tier becomes a **group** of choices with as many slots as the tier gives gifts. A tier with one product opens straight on that product; a tier with several shows a picker first. The product screen's action reads **Add gift 1 of 3** and fills the tray cell ringed in the accent colour; the tray's count keeps score (*1 of 3 chosen*). Tiers are claimed whole: **Add to cart** writes every completed tier and leaves a half-picked one open. ## Settings All on the **Gift with purchase** app embed, under **Use the Discount Kit quickview (beta)**: - **Message** and **Add to cart label** — the dialog's heading and its add-all button. - **Add default gifts when the quickview is closed** (off by default). Closing without finishing still claims the gifts: whatever the shopper picked stays, and every slot they left empty is filled with the first available option its tier offers. The lines are written through the same path as every other gift, so the selector doesn't reopen and the gifts behave exactly as if they had been chosen. Leave it off if you rely on **Stop showing after** — a close then means "no, thanks", as it always has. - **Show a "Change gifts" button in the cart** (off by default). Places a [``](/docs/components/gift-selector/web-component/) trigger under every checkout button while a claimed gift has a real choice. Its **text** and **style** (match the theme's checkout button, or a Discount Kit button in your brand colours) are settings. - **Show on cart page only** and **Stop showing after** gate the automatic opens exactly as they did for the classic modal. A shopper's own click on *Change gifts* is never gated. ## Using it alongside the Quickview Both are one dialog, so they coexist without any wiring: - **One dialog per page.** A product-card quickview trigger and the gift selector share the same element. Whichever opens last takes the dialog over; a gift selector opening on top of a quickview add is the normal "you just unlocked a gift" moment. - **Adds from the quickview count.** When a shopper adds from a quickview and the cart crosses a tier, the embed runs its cart pass and opens the gift selector — from inside the same dialog if the quickview is still open (`closeOnAdd` off), or fresh if it closed. Nothing to configure. - **Styling is shared.** Brand tokens, the quickview's CSS parts and its labels reach the gift selector unchanged; the selection tray and the change-gifts button add their own [tokens and parts](/docs/components/gift-selector/styling/) on top. - **Product data is shared.** Whatever product source the quickview uses — the Storefront API with swatches, the Ajax API, or a Liquid seed — the gift selector uses for gift products. High-variant gift products get the same availability handling. - **Cart writes are separate.** The quickview writes through the theme's standard cart actions or `/cart/add.js`. The gift selector's writes go through the embed's own cart path (the one that also removes lapsed gifts), so gift lines always carry the properties the discount function looks for. The Gift with purchase embed opens the selector; a quickview trigger is placed by you. If you want to open the selector yourself, that's the [JavaScript API](/docs/components/gift-selector/javascript-api/). ## Common set-ups **Ask once, default the rest.** *Add default gifts when closed* on, *Show a "Change gifts" button* on. The selector opens when a tier unlocks; a shopper who closes it still gets their gifts, and can change their mind from the cart. The least friction with a choice still on offer. **Never interrupt the product page.** *Show on cart page only* on, the change button on. Gifts are picked when the shopper is already in the cart, and never over a product they are reading about. Pair with *Add default gifts when closed* so a shopper who heads straight to checkout from a drawer still has their gifts. **Pick a colour.** A tier whose rule names one product's variants — the gift bottle in any of three colours. The selector opens straight on the product with only those colours offered; no picker screen, no back arrow. **Tiered gifts.** "One gift at $50, two more at $100." Each tier is its own captioned section of the tray. A shopper at $100 can claim tier 1 without touching tier 2, and when they come back for tier 2 the first tier is already in the cart. **Gift from a collection.** A tier that offers any of six products. The picker shows the six as cards; picking one opens its options; the tray fills as they go. Cards that have nothing purchasable are disabled rather than hidden, so the offer still reads as six. **Your own cart drawer.** Turn the automatic button off and drop `` into the drawer's footer with your own markup; it hides itself when there is nothing to change. See [Web Component](/docs/components/gift-selector/web-component/). **A custom gift experience.** Turn the selector off, listen to the embed's `discount_kit:gift_adjustments`, and open selection mode yourself with your own copy and layout rules — the [JavaScript API](/docs/components/gift-selector/javascript-api/#recipes) has a worked example. ## Changing gifts The change trigger reopens the dialog on the claimed tiers with the gifts already in the cart pre-filled. The action reads **Change gift 2 of 3** on a slot that already holds a gift and stays disabled while the selected options are the gift that is already there; **Add to cart** then replaces the old lines with the new ones in a single cart write. ## See also - [Gift with purchase](/docs/discounts/gift-with-purchase/) — setting up the discount itself, tiers and the classic selector. - [Quickview](/docs/components/quickview/overview/) — the dialog underneath, and everything it does with products and variants. --- # Styling Source: https://discountkit.app/docs/components/gift-selector/styling/ The dialog itself is the quickview, so its [brand tokens and CSS parts](/docs/components/quickview/styling-and-data/) apply unchanged. This page covers what the gift selector adds: the change-gifts button and the selection tray. ## The Change gifts button The embed's **Change gifts button style** setting picks how the automatic button dresses: - **Match the theme's checkout button** (default) — it borrows the checkout control's own classes, so it looks like the button it sits under. - **Discount Kit button** — a secondary button drawn in your brand tokens (accent colour, radius, font), independent of the theme's button styles. This is also what an empty `` tag renders. Either way the button carries `.dk-change-gifts`, and the element wrapping it is ``, for your own CSS: ```css dkl-change-gifts { margin-top: 1rem; } .dk-change-gifts { text-transform: uppercase; } ``` A trigger with your own markup inside is styled by your theme; nothing below applies to it. ### Tokens The Discount Kit button reads these. Set them on `:root` in your theme's CSS, for example its **Custom CSS** (**Customize → Theme settings → Custom CSS**): | Token | Default | What it styles | | --- | --- | --- | | `--dkl-change-gifts-margin` | `0.75rem 0 0` | space above the button | | `--dkl-change-gifts-width` | `100%` | button width | | `--dkl-change-gifts-min-height` | `2.75rem` | button height | | `--dkl-change-gifts-padding` | `0.75rem 1.25rem` | button padding | | `--dkl-change-gifts-border` | `1px solid var(--dkl-color-accent)` | border | | `--dkl-change-gifts-radius` | `var(--dkl-radius)` | corners | | `--dkl-change-gifts-background` | `transparent` | background | | `--dkl-change-gifts-background-hover` | an 8% accent tint | hover background | | `--dkl-change-gifts-color` | `var(--dkl-color-accent)` | text colour | | `--dkl-change-gifts-font-size` | `14px`, scaled to the browser's font size | text size | | `--dkl-change-gifts-font-weight` | `600` | text weight | ```css :root { --dkl-change-gifts-background: var(--dkl-color-accent); --dkl-change-gifts-color: #fff; --dkl-change-gifts-border: none; } ``` ## The selection tray For structural restyling, the dialog host exposes these `::part()` hooks in selection mode, alongside the quickview's own: ```css dkl-quickview[data-dkl-dialog-host]::part(tray) { background: #fafafa; } dkl-quickview[data-dkl-dialog-host]::part(slot-target) { outline: 2px solid currentColor; } ``` #### `tray` `CSSPart` The tray under the panels: heading, count and slot cells. `tray-heading` and `tray-count` for its pieces. #### `slots` / `slot-group` / `slot-group-heading` `CSSPart` The cell row, one section per tier, and a section's caption (shown with two or more tiers). #### `slot` `CSSPart` One cell. Also carries `slot-empty` or `slot-filled`, and `slot-target` on the cell the product screen's action will fill. `slot-open` is its button, `slot-remove` the × on a filled cell. #### `selection-actions` / `add-all` `CSSPart` The pinned bar under the tray and its add-all button (which also carries `add-to-cart`). #### `product-card-chosen` `CSSPart` A picker card already filling a slot. ## Wording The dialog's text for the gift flow comes from the [labels](/docs/components/gift-selector/javascript-api/#labels); the button's text, the dialog's heading and its add-all button come from the embed's settings. --- # Web Component Source: https://discountkit.app/docs/components/gift-selector/web-component/ ## There is no gift-selector element The Gift Selector has **no `` tag**. It is the [quickview](/docs/components/quickview/web-component/) dialog opened in its selection mode, and nothing you place opens it for a tier: the Gift with purchase app embed does that itself when the cart unlocks a gift. A `` trigger never opens the gift selector. The one element you place for it is **``** — the cart trigger that reopens the dialog on the gifts already claimed so the shopper can swap them. It never opens a plain product quickview: the two triggers share the dialog, not the job. To open selection mode with your own groups and slots, use the [JavaScript API](/docs/components/gift-selector/javascript-api/). ## `` A **trigger**: a click reopens the [gift selector](/docs/components/gift-selector/overview/) on the tiers the shopper has already claimed, with the gifts currently in the cart pre-filled, so they can change them. It shows only while there is something to change and hides itself otherwise, so it needs no guard in your Liquid. With the embed's **Show a "Change gifts" button in the cart** setting on, one is placed for you after `button#checkout` (Dawn and Horizon) or any `[name="checkout"]` control, and kept there through the theme's cart re-renders. You can place your own instead of, or as well as, that one. ## Usage Wrap whatever should open the dialog — a button, a link, an icon: ```html ``` An empty tag renders a plain button, with `data-label` as its text: ```html ``` That default button is the one the embed's **Discount Kit button** style also uses; see [Styling](/docs/components/gift-selector/styling/) for its tokens. :::note[What happens to clicks inside the trigger] - A click on an `` inside it opens the dialog **instead of** navigating. - Anything inside marked `data-dkl-change-gifts-ignore` is exempt: clicks there behave normally. ::: ## When it shows The element is hidden unless a claimed gift tier offers a real choice — more than one product, or more than one variant the tier permits. It stays hidden when: - the Gift with purchase embed isn't enabled, or **Use the Discount Kit quickview** is off; - the embed's **Show a "Change gifts" button in the cart** setting is off (the setting enables the behaviour for every trigger, not just the automatic one); - every claimed gift has a single option; - the cart holds no gifts. On a page that loads with gifts already in the cart it is visible from the first paint; after that it follows every cart change. ## Attributes | Attribute | Purpose | | ------------ | ---------------------------------------------------------------- | | `data-label` | Text of the default button an empty tag renders (default "Change gifts") | | `hidden` | Managed by the element from the embed's decision — don't set it yourself | ## For developers The element reads the embed's decision and asks it to open: - `window.discount_kit.core.canChangeGifts()` — whether there is something to change right now. - `window.discount_kit.core.openChangeGifts()` — open the gift selector on the claimed tiers. Returns `false` when there is nothing to change. - `discount_kit:gift_choices` on `document`, with `detail.changeable`, after every cart pass — the element listens to it; you can too. The [JavaScript API](/docs/components/gift-selector/javascript-api/) covers opening selection mode with your own groups and slots. --- # Styling Source: https://discountkit.app/docs/components/global-styling/ All component styling is driven by `--dkl-*` **CSS styling tokens** (CSS custom properties), each with a built-in fallback. There are two layers: - **Global brand tokens** (`--dkl-color-*`, `--dkl-radius`, `--dkl-font-family`) — shared by every component, so setting one re-themes all of them at once. - **Per-component tokens** (`--dkl-price-*`, `--dkl-vp-*`, `--dkl-badge-*`, `--dkl-tr-*`) — fine control over a single component; documented on each component's **Styling & data** page. Because tokens inherit through the shadow boundary, you can set them anywhere above a component — a `:root` rule in your theme's **Custom CSS** (**Customize → Theme settings → Custom CSS**) or stylesheet, an inline `style` on the tag, or the `style` attribute an app block writes. ```css /* e.g. in your theme's Custom CSS, or its stylesheet */ :root { --dkl-color-accent: #6d28d9; --dkl-radius: 12px; --dkl-price-sale-color: #c0392b; } ``` ## Global brand tokens Set them in **Discount Kit → Components → Styles**; the embed emits them on `:root`. They back every component, and each component's own `--dkl--*` tokens take precedence over them for fine-grained control. #### `--dkl-font-family` default: `inherit` Component font family. Custom font families must be installed on the theme. #### `--dkl-color-text` Default text colour. #### `--dkl-color-accent` Accent / selected state. #### `--dkl-color-success` Success / savings colour. #### `--dkl-color-border` Border colour. #### `--dkl-radius` Corner radius for all components. ## Per-component style tokens Each component adds its own tokens (and, where relevant, CSS parts and light-DOM classes) on top of the global ones. See its **Styling & data** page for the full list: ## App block vs. web component :::note Inside an **app block**, the editor's styling settings are written into the `style` attribute of the element the block renders — so merchants style it entirely from the theme editor. A **web component** has no block context, so you style it by setting the same `--dkl-*` tokens in your own CSS. Every token has the same default in both places, so a web component with no overrides looks identical to a default app block. ::: ## Targeting structural elements Tokens cover colours, sizes, and spacing. For anything structural, components expose two mechanisms depending on where the element lives: - **Shadow-DOM elements** are reached with [`::part(name)`](https://developer.mozilla.org/docs/Web/CSS/::part) from any rule — parts pierce the shadow boundary. You can add states (`::part(price):hover`) but cannot descend into a part (`::part(price) span` does not work). - **Light-DOM elements** are reached with their **class**, like any other markup. They're already styled by the component's stylesheet, so override by matching the class and winning on specificity (or, preferably, use a token where one exists). Each component's **Styling & data** page lists the parts and classes it exposes. :::note[Money formatting] The price and volume picker reformat prices in JavaScript as tiers and variants change. To stay byte-identical to your server-rendered Liquid `money` filter, each carrier passes `shop.money_format` down as `data-money-format`, which the components render with — avoiding the locale-dependent currency prefixes (e.g. `US$`) that `Intl` would otherwise produce. ::: --- # Overview Source: https://discountkit.app/docs/components/overview/ Components are a set of `` web components that render discount UI directly in your storefront — a [volume picker](/docs/components/volume-picker/overview/), a [discount-aware price](/docs/components/price/overview/), a [sale badge](/docs/components/sale-badge/overview/), a [tiered rewards progress bar](/docs/components/tiered-rewards/overview/), and a [product quickview dialog](/docs/components/quickview/overview/). They read the [discount data Discount Kit syncs](/docs/live/introduction/) to your storefront and are themed entirely through `--dkl-*` CSS styling tokens. The page components are **server-rendered** so they paint with **no flicker**; the quickview builds its dialog on the client when it opens, prefetching the product so the first open is instant. :::note[Open beta] Components are in open beta: every store has the **Components** section in the Discount Kit admin, and the **Components (beta)** app embed and **Component (beta)** block in the theme editor. They're ready to use while we smooth out the details; if something doesn't look right, contact support. ::: :::note Components show the discounts Discount Kit syncs to your storefront. Automatic discounts sync by default; codes and third-party discounts need the `dk:live` tag. See [how syncing works](/docs/live/introduction/). ::: ## Enable the Components app embed Every component depends on the **Components** app embed. Enable it once, per theme: 1. **Open the theme editor** In the Shopify admin, go to **Online Store → Themes** and click **Customize**. 2. **Find the app embeds** Open **App embeds** in the left sidebar. 3. **Enable Components** Toggle **Components** on, then click **Save**. The embed loads the shared runtime (styles + behaviour) site-wide and pre-renders the data each component needs. Without it enabled, the components have nothing to render from. :::note The embed also has a **Debug mode** toggle, which logs the standard storefront events and every component event (mount/unmount, tier-change, price-change) to the browser console. Brand colours, radius and font are set in the admin under **Discount Kit → Components → Styles**, which also takes your own `--dkl-*` [styling tokens](/docs/components/global-styling/). ::: ## Two ways to place a component Each component can be added either as a **theme app block** (placed in the theme editor) or as a **web component** (dropped straight into theme markup). They render identically — the same defaults back both paths. Build a component in the admin under **Discount Kit → Components**, then add the **Component** block in the theme editor and pick it. One block places every component type. This is the right choice for most merchants, and the only way to render in **product cards** on collection pages. (The [quickview](/docs/components/quickview/overview/) has no app block — its trigger is your own markup.) Drop the element directly into your theme's Liquid — e.g. ``. On a product page it needs **zero attributes**: it renders from a pre-staged carrier with no flicker. For developers customizing themes directly. See [Components & the app block](/docs/components/component-entries/) for the admin flow, and [App blocks vs. Web Components](/docs/components/app-blocks-vs-web-components/) for a full comparison and how to choose. ## Prerequisites Beyond enabling the **Components** app embed and tagging any codes or third-party discounts `dk:live`, components need one thing from your **theme**: support for Shopify's [**Standard Storefront Events**](https://shopify.dev/docs/storefronts/themes/best-practices/standard-events). The components react to shopper actions — selecting a variant, changing the cart — by listening for these events, so your theme must **publish** them for the components to update in response: - **`shopify:product:select`** drives variant-aware updates: - the [price](/docs/components/price/overview/) re-prices on a variant change - the [volume picker](/docs/components/volume-picker/overview/) re-calculates the selected tier on variant change - the [badge](/docs/components/sale-badge/overview/) looks up its sale's offer for the new variant - **`shopify:cart:lines-update`** drives cart-level updates: - the [tiered rewards](/docs/components/tiered-rewards/overview/) progress bar updates as the cart changes. This new way of tracking standard storefront events ensures that the components work consistently across all themes, once the new events are fully adopted by developers, rather than relying on theme specific event systems. Shopify's Horizon v4.0.0+ comes with built in support for these. For older themes, follow Shopify's [Standard Storefront Events](https://shopify.dev/docs/storefronts/themes/best-practices/standard-events) guide to integrate the events into your theme. [Shopify's Standard Actions](https://shopify.dev/docs/storefronts/themes/best-practices/standard-actions) are globally distributed across all Shopify storefronts already, so you don't need to worry about integrating them into your theme unless you need to define custom actions. :::note The components still **render** without the standard events — they're server-rendered from their carrier or app block, so they paint with no flicker regardless. They just won't **react** to variant or cart changes until your theme publishes the events. ::: ## Theming All component styling is driven by `--dkl-*` CSS styling tokens — global brand tokens shared by every component, plus per-component tokens for fine control. See [Styling](/docs/components/global-styling/) for the full token reference, how to apply them, and how to target structural elements. ## The components --- # App block Source: https://discountkit.app/docs/components/price/app-block/ The **Component** app block renders `` with no code. You build the price component once in the Shopify admin — colours, sizes, the savings badge — then pick it in the block. It's available on **product templates**. ## Add it 1. **Create the component** In the Shopify admin, go to **Discount Kit → Components** and choose **Create component**, then **Static component → Price**. The price isn't tied to one discount: it shows whatever applies to the product. Customise it against the live preview and save it. 2. **Open the theme editor** **Online Store → Themes → Customize**. Make sure the **Components** app embed is enabled. 3. **Add the Component block** In the section where you want the price, click **Add block** and choose **Component**, then position it where the price should appear. To avoid showing two prices, hide your theme's built-in price element there. 4. **Pick your component** Set the block's **Component** setting to the one you created. :::tip[Pairing with the volume picker] Leave the [volume picker's](/docs/components/volume-picker/overview/) price element selector blank so it connects to the `` element by default, and let the price component be the single element that updates the displayed price. ::: ## What you can configure Everything below is set on the component in the admin, not on the block. Each control maps to a `data-*` attribute or a `--dkl-*` token underneath — see [Styling & Data Attributes](/docs/components/price/styling-and-data/) for the equivalents. | Group | What's in it | | --- | --- | | **Layout** | Margins, the gap between the amounts, the savings badge's padding and corner radius | | **Colours** | The regular and discounted price, the struck-through compare-at, the savings badge's background and text | | **Typography** | Price size and weight, compare-at weight, badge size and weight, font family — each size with its own mobile value | | **Text** | The savings label ("Save") and the extra-discount label ("+ an extra") | | **Behaviour** | Whether the savings badge shows, and whether it reads as a percentage or an amount | :::note A block with no component picked renders nothing. Placing a component somewhere it can't find what it needs — a product, or its discount — renders nothing too. ::: ## Next steps --- # Events Source: https://discountkit.app/docs/components/price/events/ `` is event-driven. It has **no direct reference** to the [volume picker](/docs/components/volume-picker/overview/) — instead it listens for the picker's tier-change event and the theme's variant-change events, and updates the price accordingly. ## Listens for #### `discount-kit-live:volume-discount:tier-change` `CustomEvent` Emitted by the [volume picker](/docs/components/volume-picker/overview/). Matched by product id — a selected tier shows the discounted price, a `null` tier reverts to base. Coupling is by event name and product id only, with no import between the two components. Honours the event's `updatesPrice` flag: if the picker's **"Update price when a tier is selected"** setting is off, `` **ignores the tier for pricing** (the event still fires for other listeners). It also reacts to the host theme's **variant-change** events: on a variant change it updates the base price and re-applies the active tier, recomputing the discount off the new variant's price (sale-priced variants included). ## Emits `` emits a **price-change** event whenever its displayed price updates, plus **mount / unmount** lifecycle events. All bubble, so you can listen on `document`. :::note[`widgetId` and `variantId`] `widgetId` is the **app block's id** — it's `null` on a web component, `variantId` is the **current** variant and updates as the shopper switches variants (not just the variant at page load). ::: ### Price change #### `discount-kit-live:price:change` `CustomEvent` Fired whenever the displayed price changes — when a volume tier is selected or deselected, or the variant changes. **Not** fired on initial mount (the server-rendered price is already in the DOM), and **deduped** — a recompute that lands on the same price (e.g. switching to an equally-priced variant) doesn't fire. The detail carries `resource` with the previous and new price, mirroring the tier event's `previousTier` / `currentTier`: ```ts resource: { widgetType: 'price' widgetId: string | null productId: number | null variantId: number | null previousPrice: PriceState | null // null on the first change after mount currentPrice: PriceState } type PriceState = { priceCents: number // displayed price (discounted when a tier is active) compareAtCents: number | null // the struck-through "before" price, or null discounted: boolean // whether a volume tier is applied tier: VolumeDiscountTier | null // the active tier, or null } // VolumeDiscountTier: { index, quantity, discountAmount, discountType, unitPriceCents } ``` A full example payload — selecting a tier that moves the price from £20.00 to £18.00: ```js // event.detail.resource on price:change { widgetType: 'price', widgetId: 'a1b2c3d4', // the app block's id (null on a web component) productId: 7820000000001, variantId: 41200000000001, // the current variant — updates on a variant change previousPrice: { priceCents: 2000, compareAtCents: null, discounted: false, tier: null }, currentPrice: { priceCents: 1800, compareAtCents: 2000, // the original price, now struck through discounted: true, tier: { index: 1, quantity: 3, discountAmount: 10, discountType: 'percentage', unitPriceCents: 1800 } } } ``` ### Lifecycle The mount / unmount events fire as the instance's behaviour attaches and tears down — handy for wiring up (and cleaning up) custom integrations per instance. Both carry a `DklWidgetEventDetail` resource. #### `discount-kit-live:widget:mount` `CustomEvent` Fired once the controller has attached its behaviour to the (already server-rendered) element. `resource`: `{ widgetType, widgetId, productId?, variantId? }` — `productId` and `variantId` are included when known. #### `discount-kit-live:widget:unmount` `CustomEvent` Fired when the instance tears down — e.g. the element is removed, or the theme editor re-renders the section. Same `resource` shape. Use it to detach any listeners or observers you set up on mount. ```js // event.detail.resource on widget:mount / widget:unmount { widgetType: 'price', widgetId: 'a1b2c3d4', // the app block's id (null on a web component) productId: 7820000000001, variantId: 41200000000001 } ``` ## Next steps --- # Discount Price Source: https://discountkit.app/docs/components/price/overview/ ![DKL Price component](/images/components/dkl-price-component.jpg) *DKL Price component* `` renders a product's price in its own shadow DOM rather than hijacking the theme's price element — so it fully controls the markup and styling. It's **event-driven**: it has no direct reference to the [volume picker](/docs/components/volume-picker/overview/), but reacts to the tier-change events the picker emits. - When a [volume tier](/docs/components/volume-picker/overview/) is selected (matched by product id), it shows the **discounted price** as the main amount and the **original price struck through** — the compare-at when the variant is on sale, otherwise the regular price. Deselecting reverts to the base price. - On a **variant change** it updates the base price and re-applies the active tier, recomputing the discount off the new variant's price (sale-priced variants included). While a tier is active the component sets `data-dkl-discounted` on itself, so CSS can colour the discounted price distinctly. An optional **savings badge** shows how much the shopper saves. ## On this component :::note Both placements need the **Components** app embed enabled, and render identically — the same defaults back both. See [App blocks vs. Web Components](/docs/components/app-blocks-vs-web-components/) for how to choose. ::: ## Accessibility - **Labelled amounts.** Each amount is preceded by a visually-hidden label (`::part(vh)`) — "Price" for the main amount, "Regular price" for the struck-through compare-at — so screen readers can tell the live price from the original. Don't remove or repurpose `::part(vh)`. - **Live price announcements.** When a variant or tier changes, the new price is announced through a managed `aria-live="polite"` region — concise, e.g. "£18.00, was £24.00, Save 25%". It fires only on a change, never on initial load, and lives on a dedicated region rather than the host (a live region wrapping shadow DOM is monitored unreliably across screen readers). - **Contrast.** When restyling the savings badge, keep `--dkl-price-savings-color` and `--dkl-price-savings-background` at sufficient contrast. --- # Styling & Data Attributes Source: https://discountkit.app/docs/components/price/styling-and-data/ This is the shared reference for `` — the `data-*` attributes that drive its behaviour, the `--dkl-*` tokens that style it, and the CSS parts you can target. The [app block](/docs/components/price/app-block/) settings map onto these; on a [web component](/docs/components/price/web-component/) you set them directly. :::note[Using this reference with a component] A [component](/docs/components/component-entries/) built in the admin renders its own `` and writes these values for you, so what applies depends on the kind of override. **Data attributes — No.** The component sets every `data-*` itself, from its settings and the resolved discount. Adding them to your own markup has no effect on a component instance; they're the configuration surface for the [web component](/docs/components/price/web-component/) only. To change how a placed component behaves, edit it in **Discount Kit → Components**. **CSS styling tokens — Yes.** They're custom properties that inherit through the shadow boundary, so they style a component instance exactly like a bare tag. The component's editor just writes the matching tokens into the element's `style` attribute. Any token the editor doesn't expose you can still set from your theme's CSS — `:root`, or a rule targeting the element. **CSS parts — Yes.** The shadow DOM is identical, so `::part()` works the same either way. ::: :::tip[Token precedence] A token the block writes from a chosen setting is scoped to the block's id (`#dkl-price-{block.id}`), which is more specific than a `:root` rule — so to override *that* token, either change the editor setting or match the specificity (target the block element). Tokens the block leaves unset fall back to your CSS or the built-in default as normal. ::: ## Data attributes All optional. The price itself, the compare-at and the money format come from the carrier, so only the attributes you set are listed. #### `data-show-savings` `"true" | "false"` · default: `false` Show the savings badge (saving versus the original / highest price). #### `data-savings-format` `"percentage" | "amount"` · default: `percentage` How the savings badge displays the saving. #### `data-savings-label` `string` · default: `Save` The badge's leading label, e.g. "**Save** 15%". #### `data-savings-extra-label` `string` · default: `+ an extra` Connector between the sale saving and the tier saving, e.g. "Save 10% **+ an extra** 15%". #### `data-dkl-discounted` `presence` · default: `unset` **Set by the component** while a tier is active — a styling hook only. Don't author it yourself. ## CSS styling tokens ![Descriptive alt text](/images/components/dkl-price-css-tokens.jpeg) *CSS styling tokens for the DKL Price component* `` has no shadow `