LuxeDocsv0.3.0
RECIPESOptional

Theme code recipes

Optional snippets for developers who customise a theme's own cart.

For developers only — optional

Luxe never requires editing theme code. It works out of the box through the app embed and theme app blocks, and merchants are never asked to paste code. These recipes are for agencies and developers who choose to customise a theme's own cart. Each is independent and safe to remove. For no code at all, use the Luxe cart drawer: it shows set contents and the rewards bar itself.

Set contents in the theme cart

Lists a set line's components from Shopify's cart alone, so it works with the embed off. Place it inside the cart's line-item loop (Dawn 16: snippets/cart-drawer.liquid and sections/main-cart-items.liquid, after the properties list; Horizon: snippets/cart-products.liquid, replacing its own bundle list). The CSS goes in the theme's stylesheet, or in a <style> block printed once, before the first list.

{%- if item.item_components.size > 0 -%}
  <ul class="luxe-kit-contents" role="list" aria-label="{{ item.product.title | escape }}: contents">
    {%- for component in item.item_components -%}
      <li class="luxe-kit-contents__item">
        <span class="luxe-kit-contents__media">
          {%- if component.image != blank -%}
            {{- component.image | image_url: width: 96, height: 96, crop: 'center' | image_tag: alt: '', loading: 'lazy', width: 40, height: 40 -}}
          {%- endif -%}
        </span>
        <span class="luxe-kit-contents__text">
          <span class="luxe-kit-contents__title">{{ component.product.title | escape }}</span>
          {%- unless component.product.has_only_default_variant -%}
            <span class="luxe-kit-contents__variant">{{ component.variant.title | escape }}</span>
          {%- endunless -%}
        </span>
        {%- if component.quantity > 1 -%}
          <span class="luxe-kit-contents__qty" aria-label="Quantity {{ component.quantity }}">× {{ component.quantity }}</span>
        {%- endif -%}
      </li>
    {%- endfor -%}
  </ul>
{%- endif -%}

If the Includes property also shows, skip it as the first line inside the properties loop (use the merchant's Picks label if it is not "Includes"):

{%- if property.first == 'Includes' and item.item_components.size > 0 -%}{%- continue -%}{%- endif -%}

Rewards bar in a theme drawer

Dawn 16: snippets/cart-drawer.liquid, first child of .cart-drawer__footer. Horizon: snippets/cart-drawer.liquid, inside .cart-drawer__summary, above the cart-summary render.

<luxe-rewards-bar data-layout="full" data-skip-subtree-update>
  <script type="application/json" data-luxe-config>
    { "labels": {} }
  </script>
</luxe-rewards-bar>
<span hidden data-luxe-cart-state="{{ cart.item_count }}:{{ cart.total_price }}"></span>
AttributeValues
data-layoutfull or slim
data-luxe-presetOptional palette: rosewood, champagne, obsidian, eucalyptus, amethyst
data-skip-subtree-updateNeeded for Horizon's morph
data-luxe-cart-stateThe marker the cart events script compares; keep it in the part of the drawer the theme re-renders

Keep a theme cart in step

One script in layout/theme.liquid, just before </body>, refreshes the theme's cart after Luxe changes it, and opens the theme drawer after a set is added (Dawn, Horizon and two premium theme families). It needs the data-luxe-cart-state marker.

<script>
  (function () {
    function shown() {
      var marker = document.querySelector('[data-luxe-cart-state]');
      return marker ? marker.getAttribute('data-luxe-cart-state') : null;
    }

    // Premium themes by family (Shopify.theme.schema_name): Archetype's refresh their drawer on
    // cart:build and open it on cart:open; Maestrooo's refresh on cart:refresh, and their drawer
    // (#cart-drawer) opens with show().
    var ARCHETYPE = ['Impulse', 'Motion', 'Streamline', 'Expanse', 'Fetch', 'Mojave', 'Vino', 'Gem'];
    var MAESTROOO = ['Prestige', 'Impact', 'Focal', 'Warehouse', 'Stretch', 'Allure'];
    function family() {
      var name = (window.Shopify && Shopify.theme && Shopify.theme.schema_name) || '';
      return ARCHETYPE.indexOf(name) >= 0 ? 'archetype' : MAESTROOO.indexOf(name) >= 0 ? 'maestrooo' : null;
    }

    function themeEvent(name) {
      document.documentElement.dispatchEvent(new CustomEvent(name, { bubbles: true }));
    }

    function horizonCarts() {
      return Array.prototype.slice.call(document.querySelectorAll('cart-items-component[data-section-id]'));
    }

    function refreshThemeCart() {
      if (horizonCarts().length > 0) {
        return import('@theme/section-renderer').then(function (mod) {
          return Promise.all(horizonCarts().map(function (el) {
            return mod.sectionRenderer.renderSection(el.dataset.sectionId, {
              cache: false,
              mode: el.closest('cart-drawer-component') ? 'hydration' : 'full'
            });
          }));
        });
      }
      if (family() === 'archetype') return Promise.resolve(themeEvent('cart:build'));
      if (family() === 'maestrooo') return Promise.resolve(themeEvent('cart:refresh'));
      if (typeof publish === 'function' && typeof PUB_SUB_EVENTS !== 'undefined') {
        return Promise.resolve(publish(PUB_SUB_EVENTS.cartUpdate, { source: 'luxe' }));
      }
      if (/\/cart\/?$/.test(location.pathname)) location.reload();
      return Promise.resolve();
    }

    function openThemeDrawer() {
      var dawn = document.querySelector('cart-drawer');
      if (dawn && typeof dawn.renderContents === 'function') {
        var root = (window.Shopify && Shopify.routes && Shopify.routes.root) || '/';
        return fetch(root + '?sections=cart-drawer,cart-icon-bubble')
          .then(function (res) { return res.json(); })
          .then(function (sections) {
            dawn.classList.remove('is-empty');
            dawn.renderContents({ sections: sections });
          });
      }
      var horizon = document.getElementById('cart-drawer');
      if (horizon && typeof horizon.open === 'function') {
        return refreshThemeCart().then(function () { horizon.open(); });
      }
      if (family() === 'archetype' && document.getElementById('CartDrawer')) {
        themeEvent('cart:build');
        themeEvent('cart:open');
        return Promise.resolve();
      }
      if (horizon && typeof horizon.show === 'function') {
        return refreshThemeCart().then(function () { horizon.show(); });
      }
      return Promise.resolve();
    }

    document.addEventListener('luxe:cart:updated', function (event) {
      var d = event.detail || {};
      var state = shown();
      if (state === null || state === d.itemCount + ':' + d.totalPrice) return;
      refreshThemeCart();
    });

    document.addEventListener('luxe:kit:added', function (event) {
      // Only where a theme drawer can open; otherwise Luxe goes to the cart page.
      var drawer = document.getElementById('cart-drawer');
      var can = document.querySelector('cart-drawer, cart-drawer-component') ||
        (family() === 'archetype' && document.getElementById('CartDrawer')) ||
        (drawer && typeof drawer.show === 'function');
      if (!can) return;
      event.preventDefault();
      openThemeDrawer();
    });
  })();
</script>

Theme detection: Horizon re-renders through its @theme/section-renderer import map entry, Dawn through its cart-update pub/sub event, Archetype themes (Impulse, Motion, Streamline, Expanse and others) through cart:build, and Maestrooo themes (Prestige, Impact, Focal, Warehouse and others) through cart:refresh. Other themes reload the cart page; a drawer needs the theme's own refresh call in refreshThemeCart.

Loop safety: luxe:cart:updated fires on every cart read Luxe makes. The script refreshes only when the drawer's data-luxe-cart-state marker differs from the event's itemCount and totalPrice, so a drawer that already shows the cart never re-renders again.

Edit set on a theme cart line

On the theme's cart page Luxe adds Edit set by itself, with no code. For other line-item loops:

{%- if item.properties._luxe_kit != blank or item.properties._luxe_kit_group != blank -%}
  <button type="button" class="luxe-edit-kit" data-luxe-edit-line="{{ item.key | escape }}" hidden>Edit set</button>
{%- endif -%}

Hidden until Luxe reveals it on lines it can edit.

Review stars

No code: the builder reads the standard reviews.rating and reviews.rating_count product metafields that review apps write.

Was this page helpful?Last updated 4 October 2026