Developer documentation

Ignis Pack Builder for theme developers.Contract v1

Last updated 9 September 2026. Applies to the Ignis app for Shopify.

A custom pack is one product the shopper composes: the merchant defines the items in the pack and which products and colors each item may be, the shopper picks a color and size per item, and one line lands in the cart. Ignis deducts the chosen components when the order is paid and puts them back on a refund or a cancellation that restocks.

This page is the whole contract between a theme and Ignis for packs. It is small on purpose. Everything a theme can see is listed here; everything that moves stock or money is Ignis's and is not something a theme can change.

  1. The pack product
  2. The storefront block and its settings
  3. The product template rule
  4. The cart line properties
  5. Showing the shopper their choices in the cart
  6. Showing the pack's contents on the packing slip
  7. What Publish checks
  8. How stock moves
  9. Two ways to sell a pack
  10. Works well with AI coding tools
  11. Changelog

The pack product

A pack sells as a product the merchant owns. Its shape is fixed, and Ignis can create or rebuild it for the merchant from the app's Pack builder page:

If the product carries any option beyond its pack size and one size, Publish refuses. The reason is not cosmetic: the theme's own variant picker would then let a shopper buy a bare pack with nothing chosen, and nothing would be deducted for it. A pack bought through a sales channel with no item choices (a size switcher only) is deducted automatically when that size leaves exactly one item to choose per position; otherwise the merchant is told which order to adjust by hand.

The storefront block and its settings

The block is an app block named Ignis Pack Builder, added in the theme editor to any section on the product template that accepts app blocks. It renders only on a product that has a published pack; on any other product it renders a note visible to the merchant alone.

What it draws, top to bottom: the pack size choice when the product has more than one; one color control (swatches) when two or more items offer colors; one size control when two or more items offer sizes; then a short heading (Your pack) and one row of items. Each item is a tile: its label, its picture in the chosen color, and a caption such as Stone Blue / L, with a pencil badge on the picture. Nothing else is open until the shopper clicks an item; then that item's own product choice (where an item allows several products), color swatches and size pills open under the row, and Done closes them. One item is open at a time. Then the button. The total, with the saving, sits in the header above the pack size choice. The older layout, cards with every item's pickers shown inside them, is still available as a block setting.

SettingDefaultWhat it does
use_app_appearanceonDraw with the appearance saved in the Ignis app. Off customises this placement only.
cta_labelAdd pack to cartThe button. It adds the pack to the cart, so say so; a label like "Build your pack" reads as a step before the add.
pack_size_labelPack sizeAbove the 3-Pack / 4-Pack choice, when the product has one.
colour_labelthe product's own option nameAbove the one color control, when two or more items offer colors. Blank uses whatever the product calls the option (Color, Color, Farbe).
size_labelthe product's own option nameAbove the one size control, when two or more items offer sizes. Blank uses the product's option name.
edit_labelEditThe per-item control that opens that item's own color and size. Blank reads Edit, or Edit size when only a size row is shown.
item_layouttraytray: one row of items, one opens at a time. cards: every item shows its own pickers inside a card.
edit_stylepencilpencil: a badge on the item's picture. text: a text link under it.
edit_badge_size30The pencil badge's size in pixels, 20 to 40.
tray_titleYour packThe heading above the row of items (tray layout only).
tray_hintClick an item to change itThe hint beside that heading (tray layout only).
after_addgo_to_cartAfter adding: go to the cart page, or stay on the product page. See what "stay" does.
added_labelAdded to your cart.The confirmation shown under the button when the shopper stays. Blank uses the default.
view_cart_labelView cartThe link beside that confirmation. The cart's item count follows it in brackets.
accent_color, badge_color#1b1b1bThe accent and the saving badge.
max_width, alignmentfull, leftBlock width and, when not full, its alignment.
pill_radius6Corner radius of the selector pills, in px.

Swatches come from the same three sources as Ignis Bundles: the variant image, Shopify's own swatches on the option value, or a theme file named swatch-<colour-handle>.jpg or .png. The pack-level swatch is a copy of the item's own, so the two can never disagree.

The product template rule

The block must be the only way to buy the pack product. Give the pack product its own product template with the theme's variant picker and Add to cart removed, and the Ignis Pack Builder block in their place. A shopper who buys the pack through the theme's own button gets a bare pack size with no items chosen, and Ignis deducts nothing for it, because there is nothing recorded to deduct.

The app reads the live theme and warns the merchant on the Pack builder page when the product's template still renders a product form. It warns; it does not refuse, because themes vary and the reader cannot follow every snippet.

The cart line properties

One cart line per pack, on the pack product's variant for the chosen size, quantity as many packs as the shopper wants. The line carries these properties:

PropertyVisibleValue
_ignis_packno (underscore)JSON: {"id":"<pack id>","v":<definition version>,"c":[<variant id>, …]}. c is one component variant id per item, in item order, with repeats when two items are the same variant. The block writes the ids as numbers; Ignis also accepts them as strings. This is what Ignis deducts from and what a Cart Transform expands. Never rewrite it.
one per item, named by the item's labelyesThe key is the label the merchant gave the item in the editor (the defaults are Item 1, Item 2, …). The value is the shopper's color and size for it, joined with a slash: Black / M. When an item allows several products, the value still names only the color and size; the product is in _ignis_pack.

Ignis validates c against the pack's published allowlist when the order is paid. A component the pack does not allow, a wrong count, or a pack id the store does not own is refused and logged, and never deducted. A line whose _ignis_pack is missing altogether is not a pack sale and is ignored, which is why the template rule above matters.

What happens after the add

after_add is go_to_cart by default: the block adds one line and sends the shopper to the cart page. That works on every theme with nothing to wire.

Set it to stay and the shopper stays on the product page. The block always shows its own confirmation: the button reads the added message for a moment, and a line with a View cart link stays under it until the shopper changes something. On top of that, two hooks, both optional:

document.addEventListener('cart:refresh', function (e) {
  var cart = e.detail && e.detail.cart;
  if (!cart) return;                 // the go-to-cart ending fires it without a cart
  document.dispatchEvent(new CustomEvent('cart:change', { detail: cart }));  // or whatever your drawer listens for
  openCartDrawer();                  // your theme's own function
});

The add itself never depends on either hook. A theme renderer that throws is caught, and the block's own confirmation still shows.

Showing the shopper their choices in the cart

Shopify carries the properties on the line; whether the shopper sees them is the theme's choice. Most themes already loop over line properties. If yours does not, this is the whole change, in the cart page and the cart drawer:

{%- comment -%} Show every visible line property; underscore-prefixed ones are protocol, not for the shopper. {%- endcomment -%}
{%- for p in item.properties -%}
  {%- assign key = p.first -%}
  {%- if p.last != blank and key.first != '_' -%}
    <div class="cart-item__property">{{ key }}: {{ p.last }}</div>
  {%- endif -%}
{%- endfor -%}

If your cart drawer renders from JavaScript (a fetch of /cart.js after an add, instead of a server-rendered section), it needs the same loop in JavaScript. Shopify hands the properties back on every line as item.properties; skip the keys that start with an underscore, they are protocol, not for the shopper:

function lineProperties(item) {
  var props = item.properties || {};
  return Object.keys(props)
    .filter(function (k) { return k.charAt(0) !== '_' && props[k] !== '' && props[k] != null; })
    .map(function (k) { return '<li class="cart-item__property">' + escapeHtml(k) + ': ' + escapeHtml(String(props[k])) + '</li>'; })
    .join('');
}
// ... inside your line template: '<ul class="cart-item__properties">' + lineProperties(item) + '</ul>'

A drawer that renders the line on the server (a section fetched through bundled section rendering, or the page's own Liquid) already gets this from the Liquid loop above. A store whose drawer skipped it showed the pack line with no items in it the moment the block started opening the drawer itself (2026-09-10); the cart page, rendered by Liquid, was right the whole time.

The app reads the live theme's cart files and warns the merchant on the Pack builder page when it finds no such loop. Same rule as the template check: it warns, it never refuses.

Showing the pack's contents on the packing slip

Shopify's packing slip prints a single-line pack by name only: Relaxed Core Tee 4-Pack / M, with nothing under it, because the default template prints the title, the variant, the SKU and the quantity and never loops the line's properties. Whoever packs the order cannot see which four tees go in the box. Shopify has no API for the packing slip template, so this is one paste, once per store. The canonical copy is the Copy button on the Custom pack builders page in the app; this is the same code, version 1.

What it prints, in the slot the default template gives the variant title and the SKU: every visible property as Label: value, one per line, in item order, the way the cart shows them (underscore keys stay hidden, same rule as the cart loop above). Pure Liquid: it needs nothing from Ignis to run.

It prints nothing for a pack the Cart Transform has split into its items, on purpose. Those items reach the slip as their own lines, each carrying the pack as a group in line_item.groups, and Shopify's own default template has printed Part of: <pack name> under each of them since bundles (right after the SKU). A paste that printed it again would put the pack name on paper twice per item. So one paste still covers a store that later moves from single line to the Cart Transform. If your template was customised before bundles and has no {% for group in line_item.groups %} loop, add Shopify's own: {% for group in line_item.groups %}<span class="line-item-description-line">Part of: {{ group.title }}</span>{% endfor %}, after the SKU.

  1. In the Shopify admin, open Settings, then Shipping and delivery. Under Documents, click Packing slip template.
  2. Find the line {% if line_item.variant_title != blank %} and the {% endif %} that closes it. Paste the code below on a new line right after that {% endif %}.
  3. If the template already has a block that starts with Ignis pack contents, replace that whole block instead. One block, never two.
  4. Click Preview template to check it, then Save.
{% comment %}Ignis pack contents v1. Prints what the shopper chose for a custom pack under the pack. Paste once. To update, replace everything from this line to "End Ignis pack contents". https://ignishq.com/docs/pack-builder#packing-slip{% endcomment %}
{%- for ignis_property in line_item.properties -%}
  {%- assign ignis_key = ignis_property.first | strip -%}
  {%- assign ignis_value = ignis_property.last | strip -%}
  {%- assign ignis_key_start = ignis_key | slice: 0 -%}
  {%- if ignis_key != blank and ignis_value != blank and ignis_key_start != "_" -%}
    <span class="line-item-description-line" style="display: block;">{{ ignis_key | escape }}: {{ ignis_value | escape }}</span>
  {%- endif -%}
{%- endfor -%}
{% comment %}End Ignis pack contents{% endcomment %}

A customised template that dropped Shopify's line-item-description-line class still gets one pick per line: each span carries its own display: block. Both halves are escaped, so a property written by a shopper or another app cannot put markup on a printed slip. Every variable is ignis_-prefixed, so the paste cannot clobber an assign your template already has.

What Publish checks

Publish resolves the pack against the store and refuses with one plain sentence per problem. Every sentence is the merchant's to act on; none is about the theme. The checks, in the merchant's words:

After Publish, a change to any product the pack uses re-resolves the pack. A component that stops tracking inventory or starts selling when out of stock is dropped with a warning; if that leaves an item nobody can fill, the pack is taken off the storefront and the merchant is told, rather than left live and unbuyable.

How stock moves

Two ways to sell a pack

ModeWhat the cart holdsWho prices itNeeds
Single lineOne line on the pack product's variant, with the properties above.The variant's own Shopify price.Any plan. The only mode for a product with more than one pack size. Otherwise pre-selected only when the store cannot run the Cart Transform, and the merchant picks it.
Cart TransformThe pack line expanded into its component lines at checkout by a Shopify Function.The pack's pricing rule (a fixed amount or a percentage off the components).Shopify Plus, or an app installed from the App Store. One pack size per product.

The mode is chosen by the merchant at Publish and never inferred. The block and the cart properties are identical in both.

Works well with AI coding tools

A coding assistant working inside your theme repository can build a pack UI, or the cart display above, from this page alone. Two instructions worth passing along: never construct or rewrite _ignis_pack by hand, because the value is validated against the published allowlist and a hand-built one is refused; and never expose the theme's own Add to cart on the pack product's template.

Changelog

VersionDateChange
v210 September 2026The tray: one row of item tiles with a pencil badge, one item's pickers open at a time, Done closes them; item_layout, edit_style, edit_badge_size, tray_title and tray_hint settings; the pack-level row labels default to the product's own option names; US spelling throughout.
v1.114 September 2026The packing slip paste: the pack's contents under a single-line pack on Shopify's packing slip. A split pack is already named by Shopify's own template.
v19 September 2026First published contract: the pack product shape, the block and its settings (including the pack-level color control and the per-item Edit link), the cart line properties, the cart display loop, the Publish checks, and the stock rules.

Breaking changes get a new version number and stay listed here. Additive changes are noted against the version they landed in.

Questions

Integration questions, or something on this page that does not match what you see in a store: [email protected].