JavaScript API
The gift selector is the quickview 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?
Section titled “Which API do you need?”- Just the embed’s behaviour, with your own button — place
<dkl-change-gifts>or callwindow.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
groupsandslots, and either let the dialog write to the cart or take the lines withsubmitLines. - 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) anddiscount-kit-live:quickview:add-to-cart(what was written). See Events.
Selection mode
Section titled “Selection mode”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
Section titled “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<string, string> |
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.
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
Section titled “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
Section titled “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.
Owning the write: submitLines
Section titled “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:
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
Section titled “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
Section titled “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:
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:
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:
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:
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:
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 <dkl-change-gifts>
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:
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.discountTitle, _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.
The change-gifts decision
Section titled “The change-gifts decision”When the embed drives the gift selector, these are on window.discount_kit.core:
canChangeGifts()—truewhile a claimed tier offers a choice.openChangeGifts()— reopen the selector on the claimed tiers, pre-filled.falsewhen 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; <dkl-change-gifts> follows it.
Labels
Section titled “Labels”Override any subset globally through window.DklContext.quickviewLabels, exactly as for
the quickview’s labels:
| 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 |