Where each setting lives
Every setting has exactly one home: either the Cartal app or the Cart progress app embed in the theme editor. The bar always sits at the top of the cart.
| Setting | Home |
|---|---|
| Rewards, amounts, reward names and icons | App (Rewards) |
| Bar messages (while earning, unlocked, all unlocked) | App (Rewards Settings) |
| Discount combinations and currency overrides | App (Rewards Settings) |
| Upsells, sources, empty cart suggestions, collapse when the cart fills | App (Upsells) |
| Bundles | App (Bundles) |
| Show reward markers, also show on the cart page | Theme editor (App embeds, Cart progress) |
| Bar colors (fill, track, text, marker); blank uses the theme | Theme editor (App embeds, Cart progress) |
| Sizes, fonts, spacing, animation, gift button look, upsell card layout | Theme editor (App embeds, Cart progress) |
The cart icon attribute
When Always show the cart icon is on in the Cartal app embed, Cartal setsdata-cart-pilot-cart-icon="always" on <html> from an inline script in the head, before the page paints. Cartal never restyles theme headers itself. Stock Horizon and Dawn always show the cart icon, so they need nothing.
A theme that hides its cart icon while the cart is empty should keep it visible when the attribute is set, so shoppers can open the drawer and see empty cart suggestions:
html[data-cart-pilot-cart-icon="always"] .header__cart-icon--empty {
display: revert;
visibility: visible;
}If the cart button also refuses to open an empty drawer, gate that check on the same attribute:
const alwaysOpen = document.documentElement.dataset.cartPilotCartIcon === "always";
if (cartIsEmpty && !alwaysOpen) return;Cart refresh events
Cartal changes the cart on its own when it adds, swaps, removes or caps a free gift, and when a shopper adds an upsell or a bundle. After each change it asks the theme to re-render its cart and header count. What it sends, in order of detection:
- Horizon (when
cart-items-component,cart-drawer-componentorcart-iconis on the page): the Storefront standard eventshopify:cart:lines-updateondocument(Horizon 4.x and themes built on it, such as Savor), and the legacycart:update(Horizon 1.x to 3.x) with the rendered sections. - Dawn cart drawer (
cart-drawerwithrenderContents): the drawer's own sections go torenderContents. - Dawn pub/sub:
publish(PUB_SUB_EVENTS.cartUpdate, { source: "cart-pilot", cartData }). - Every theme: a
cart:refreshevent ondocumentwith thecart:updatedetail shape. - Generic swap (no Horizon, no Dawn drawer): count bubbles such as
#cart-icon-bubble,.cart-count-bubbleand[data-cart-count], and cart line containers such ascart-itemsand#main-cart-items, are re-rendered from their section. A/cartpage where nothing could be updated reloads as a last resort.
A theme with its own cart UI can listen for any of these, or just cart:refresh, and re-render. Cartal ignores events whose detail.sourceId, detail.source or detail.data.sourceis "cart-pilot", so re-dispatching its event does not cause a loop.
The gift-added event
After a shopper confirms a free gift and it is in the cart, Cartal dispatches on document:
document.addEventListener("cart-pilot:gift-added", (e) => {
const { variantId, productId, lineKey, tierId } = e.detail; // productId and lineKey may be null
});It also sets data-cart-pilot-new for 1.5 seconds on the matching cart line (found by[data-key], [data-line-key], [data-cart-item-key],[data-item-key] or [data-line-item-key], else by variant id), which gets a subtle background flash. Themes can override [data-cart-pilot-new].
Gift button and panel
The gift button (.cbp__gift-btn) spans the full width of the bar with a 44px minimum height. It carriesdata-attn while a gift is unlocked but not chosen, and data-chosen once one is. When a reward offers a choice of gifts, the button opens a panel (.cbp-gs, role="dialog") inside the cart drawer, or a centered sheet on the cart page. Escape, the back button and the backdrop close it without changes.
| Theme editor setting | Setting id | Reaches the bar as |
|---|---|---|
| Button style: Filled (default), Outline, Soft | gift_btn_style | data-gift-style="outline" or "soft" |
| Button color (blank: the progress fill) | gift_btn_color | --cbp-gift |
| Button text color (blank: picked for contrast) | gift_btn_text | --cbp-gift-text |
| Show a gift icon (default on) | gift_icon | data-gift-icon-off when off |
| Attention effect: None, Pulse (default), Shine | gift_attention | data-gift-fx="none" or "shine" |
CSS variables and attributes
Embed settings reach the progress bar and upsell row as CSS custom properties and data attributes, set only when changed, so defaults leave your theme's values alone.
| Variable | Controls |
|---|---|
--cbp-message-size | Message text size |
--cbp-label-size | Milestone label size |
--cbp-pad-y, --cbp-pad-x | Vertical and horizontal padding |
--cbp-highlight | Amount and reward highlight color |
--cbp-earned-size, --cbp-earned-color | Unlocked line size and color |
--cbp-height | Bar thickness (1 to 8px) |
--cbp-marker-size | Milestone marker size (8 to 24px) |
--cbr-gap | Space between upsell cards |
--cbr-count-width | Card width when Cards per row is set |
data-motion="off": the embed's animate setting is off. Shoppers who prefer reduced motion never see animation either way.data-cbr-countanddata-cbr-count-phone: upsell cards per row, chosen with a container query on the row itself.data-collapsed: the upsell row is collapsed (its heading is a disclosure button witharia-expanded).
Building a theme and something does not fit? Contact support.