# Magic Box

> A box the customer fills with products you choose, under rules you set, that reaches the cart and checkout as one line with its own title, image and discount.

Status: Ready (Smootify 2.0)  
Add-on: Magic Box  
URL: https://www.smootify.io/docs/astro/features/magic-box  
Updated: 2026-10-21

Magic Box lets the customer build a box: a case of 6 wines, a sampler of 3 coffees, a PC with one part from each category. The customer picks products from the lists you design, and the box goes to the cart as **one line**, with its own title and image. At checkout it stays one line, and it can carry a discount. This page covers the Shopify side, the box, its rules, categories and the box panel.

> [!NOTE]
> Turn on **Magic Box** in the Smootify dashboard of the site. It is included in every plan, the free one too, but it always needs to be turned on. Turning it on adds the **Magic Box** metaobject definition to Shopify and the checkout function that merges the box into one line.

## How it fits together

Everything goes inside one element, `smootify-magic-box`.

What Smootify needs:

- a `smootify-magic-box` element;
- inside it, the products the customer can pick, each with its own add-to-cart form. Inside the box, these forms add to the box instead of the cart;
- inside it, a `smootify-magic-box-cart`: the box panel. It holds a `box-item` template, repeated once per product in the box, and a `<form>` that adds the whole box to the cart. Without it, the box is removed.

What is up to you:

- how the products are listed: a loop over entries, several lists, a slider;
- where the box panel sits (a sidebar, a sticky bar, under the products) and what it shows;
- the layout, classes and styles of every part.

A fixed box of 6 looks like this:

```html
<smootify-magic-box data-min="6" data-max="6">
  <div class="sm-dyn-item">
    <smootify-product data-id="1234567890">
      <div product="title">Name</div>
      <smootify-add-to-cart>
        <form><input type="submit" value="Add to box"></form>
        <div form-fail hidden>Something went wrong</div>
      </smootify-add-to-cart>
    </smootify-product>
  </div>
  <smootify-magic-box-cart>
    <items-list>
      <box-item><div box-item="title">Product</div><div box-item="quantity">1</div><button type="button" data-action="remove">Remove</button></box-item>
    </items-list>
    <empty-state>Your box is empty</empty-state>
    <form><input type="submit" value="Add the box to cart"></form>
    <div form-done hidden>Added</div>
    <div form-fail hidden>Something went wrong</div>
  </smootify-magic-box-cart>
</smootify-magic-box>
```

Render one product card for each product of the list.

## 1. Give the box a title, an image and a discount

In the cart and at checkout, the box line shows a title and an image of its own. There are two ways to set them.

**A Magic Box entry in Shopify.** In Shopify, go to **Content > Metaobjects > Magic Box** and create an entry with a **Title**, an **Image** and, if you want one, a **Discount Percentage**. On `smootify-magic-box`, set `data-handle` to the entry’s handle. The merchant can then change the title, the image or the discount in Shopify without touching the site.

**A static box.** Set `data-title` and `data-image` on `smootify-magic-box`. The image must be a Shopify file: upload it in Shopify under **Content > Files** and copy its URL. Checkout ignores an image from anywhere else.

> [!WARNING]
> The discount can only come from a Magic Box entry. A static box has no discount.

The discount applies to every box the customer can add to the cart under its limits, not only to a full one. For a discount only on a full box, give the box the same `data-min` and `data-max`, such as `data-min="12" data-max="12"`.

The discount is a percentage off the whole box, applied at checkout.

## 2. Build the box

1. Write a `smootify-magic-box` element around the products and the box panel.
2. Inside it, list the products. Any list works: cards rendered from your product entries ([product cards](https://www.smootify.io/docs/astro/build/product-cards)), or a list loaded from Shopify ([product lists](https://www.smootify.io/docs/astro/build/product-lists)). Each card needs a `smootify-add-to-cart` with a `<form>` inside. Its button adds the product to the box. Variant pickers and a quantity field work as on the [product page](https://www.smootify.io/docs/astro/build/product-page).
3. Inside `smootify-magic-box`, write a `smootify-magic-box-cart` element. Put inside it:
   - an `items-list` element, holding one `box-item` element. The `box-item` is the template: Smootify repeats it for each product in the box. `items-list` shows only while the box has products;
   - optionally, an `empty-state` element, shown only while the box is empty;
   - a `<form>` with a submit button. It adds the whole box to the cart.

Inside `box-item`:

- `box-item="title"`, `box-item="image"`, `box-item="price"`, `box-item="total"` (price × quantity), `box-item="quantity"` and `box-item="category"` show the product in the box;
- the `product` and `variant` values also work, such as `variant="title"` for the variant name. See [box item values](https://www.smootify.io/docs/astro/reference/attributes/box-item);
- a `button` with `data-action="remove"` takes the product out of the box.

After a product goes into the box, the quantity field of its form goes back to 1.

## 3. Set the size of the box

These attributes go on `smootify-magic-box`:

| Attribute | What it does | Default |
|---|---|---|
| `data-min` | the fewest products the box can hold | 1 |
| `data-max` | the most products the box can hold | 100 (also the highest value) |
| `data-step` | the box size must be a multiple of this | 1 |
| `data-unique` | the same product can go in only once | off |

The box can be bought when the number of products is **between `data-min` and `data-max` and a multiple of `data-step`**. The add button stays disabled until then. Some sizes and the boxes they allow:

- **A fixed size.** `data-min="6"` and `data-max="6"`: exactly 6 products.
- **Sizes you choose.** `data-min="6"`, `data-max="12"`, `data-step="6"`: 6 or 12, nothing in between.
- **A free mix with a limit.** `data-max="10"` alone: from 1 to 10 products.
- **Sizes that do not share the step.** With `data-min="10"`, `data-max="25"` and `data-step="15"`, the box can be bought with 15 products only: 10 and 25 are not multiples of 15. A box of 10 or 20 needs `data-step="10"`.

> [!WARNING]
> Every size must be a multiple of the same step. A box of 1, 3 or 6 cannot be written with one step.

`data-unique` and the category rules below are on whenever they have a value, even `false`. Write `true`, and remove the attribute to turn a rule off.

When a product does not fit, the product’s form shows an error in its `[form-fail]` block. You can change the text of each error:

- `data-full-error`: the box is full. Default “Box is full”;
- `data-too-many-error`: the quantity is more than what fits. `{quantity}` in the text becomes the number that still fits;
- `data-only-one`: the product is already in the box and `data-unique` is on. Default “You can add only 1 item inside the box”;
- `data-only-one-per-category`: the category already has a product. `{category}` becomes the category name. Default “You can add only 1 item of {category} inside the box”.

## 4. One product per category

For a box that needs one piece from each group (a base, a sauce and a topping; a PC with a motherboard, a GPU and RAM), split the products into categories.

1. Wrap each group of products in a `smootify-box-category` element.
2. Give it the attribute `data-name` with the category name, such as `Sauce`.

```html
<smootify-box-category data-name="Sauce">
  <div class="sm-dyn-item">
    <smootify-product data-id="1234567890">
      <div product="title">Name</div>
      <smootify-add-to-cart>
        <form><input type="submit" value="Add"></form>
        <div form-fail hidden>Something went wrong</div>
      </smootify-add-to-cart>
    </smootify-product>
  </div>
</smootify-box-category>
```

Render one such card for each product of the category.

Then choose the rules on `smootify-magic-box`. For a pizza with a Base, a Sauce and a Topping:

- `data-unique-per-category="true"`: one product per category. A second pick in the same category shows an error in the Error message of the product's form.
- `data-auto-remove-category="true"`, with the one above: a new pick replaces the old one in its category instead of showing an error. Use it for a configurator.
- `data-all-categories="true"`: the box can be bought only when every category has a product. Until then, the add button stays disabled.

**Steps without interactions.** Put each category in a slide of a slider (`[slider-track]`, each slide one of its children), or in a panel of ARIA tabs (`[role="tablist"]` with its `[role="tab"]` buttons and `[role="tabpanel"]` panels): see [Write components with Smootify elements](https://www.smootify.io/docs/astro/build/components). When the customer adds a product, the slider or the tabs move on: to the next empty category with `data-all-categories`, otherwise to the next slide or tab. When the customer removes a product, it goes back to that product’s category. Smootify runs the `[slider-prev]`, `[slider-next]` and `[slider-dots]` placed inside the Magic Box.

**Completed categories.** A category gets the class `is-complete` while the box holds at least one of its products, and so do the slide and the `[role="tab"]` that show it when every category in them has one. To show a check mark inside a tab, put it in an element with `box-condition="category-complete"`; `box-condition="category-incomplete"` is the opposite. They work inside the category, its slide and its tab.

In the box panel, `data-prop="missingCategories"` lists the categories still empty, separated by commas. `separator` changes the separator, and `separator="repeat"` makes one copy of the element per missing category.

## 5. Show progress and the price

Inside `smootify-magic-box-cart`, outside the `box-item`, these elements follow the box:

```html
<div><span data-prop="currentQuantity">0</span> / <span data-prop="maxQuantity">6</span></div>
<div><div data-prop="leftQuantityWidth"></div></div>
<div box-condition="cannot-purchase">Missing: <span data-prop="missingCategories">Base, Sauce</span></div>
<smootify-price>
  <div data-prop="price">$0.00</div>
  <div data-prop="compareAtPrice">$0.00</div>
</smootify-price>
<div box-condition="can-purchase">You save <span data-prop="discount-amount">$0.00</span></div>
```

- `data-prop="currentQuantity"`, `"minQuantity"`, `"maxQuantity"`, `"leftQuantity"` (how many more fit): numbers;
- `data-prop="leftQuantityWidth"` on an element: its width follows how full the box is, for a progress bar. `data-prop="leftQuantityPercentage"` shows the same as text;
- `data-prop="discount-percentage"` and `data-prop="discount-amount"`: the discount of the box, as a percentage and as money;
- a `smootify-price` shows the box total after the discount in `data-prop="price"`, and the total before the discount in `data-prop="compareAtPrice"` when there is one;
- `box-condition="can-purchase"` and `box-condition="cannot-purchase"` show an element only while the box can, or cannot yet, be bought.

`smootify-magic-box-cart` also gets classes you can style. For a box of 6 wines at $20.00 each, the panel goes through these states:

| State | Classes on `smootify-magic-box-cart` | What shows |
|---|---|---|
| Empty | `is-empty`, `is-disabled` | `empty-state` instead of `items-list`, `box-condition="cannot-purchase"`; the add button is disabled |
| Filling, 4 of 6 | `is-disabled` | `items-list`, `box-condition="cannot-purchase"`; the add button is disabled |
| Ready, 6 of 6 | `is-purchasable` | `box-condition="can-purchase"`; the add button is enabled; `data-prop="price"` $120.00 |
| Ready, with a 10% discount from a Magic Box entry | `is-purchasable`, `is-saving` | `data-prop="price"` $108.00 after the discount, `data-prop="compareAtPrice"` $120.00 before it, `data-prop="discount-amount"` $12.00 |

The full lists are in [Magic Box values](https://www.smootify.io/docs/astro/reference/attributes/magic-box) and [Magic Box conditions](https://www.smootify.io/docs/astro/reference/conditions/magic-box).

**The add button.** A submit button works: an `input` or a `<button type="submit">`. It is disabled until the box can be bought. A second `<button type="submit" name="buy-now">` adds the box and goes straight to checkout. `data-buy-now="true"` on `smootify-magic-box-cart` sends every submit to checkout.

**Extra fields.** A field in the box’s form, such as a gift message, is saved with the box, under its field name, and shows with the box in the cart. A name that starts with `_` is saved but not shown.

## 6. In the cart and at checkout

The box is one line in the cart, with the box title and image. In the [cart](https://www.smootify.io/docs/astro/build/cart), a `box-item` inside the `cart-item` template repeats once per product in the box, with the cart line values such as `box-item="title"` and `box-item="quantity"`. The customer cannot change the quantity of a box: its quantity field is removed. With a `box-item` in the template, `cart-item="options"` lists only the box’s own fields, such as a gift message. Without one, it lists the products as text (“1 x Wool Socks”).

At checkout Shopify shows the box as one line, with its title, image and discount.

**Subscriptions.** A box cannot be sold as a subscription: Shopify does not merge cart lines that have a selling plan. See [subscriptions](https://www.smootify.io/docs/astro/features/subscriptions) for what you can build instead.

Developers can add a box to the cart from code with [`addBoxToCart`](https://www.smootify.io/docs/astro/reference/api/methods/addboxtocart). They can also change the box title and image as the box fills, for example “Box of 4 wines”: see [the Magic Box example](https://www.smootify.io/docs/astro/developers/events#example-a-magic-box-title-that-follows-its-contents).

## Before you publish

On the dev server and on your `*.workers.dev` address, the Smootify debugger lists the Magic Box elements and tells you when they are not running. See [the debugger](https://www.smootify.io/docs/astro/start/debugger).

> [!STUCK]
> - **Products go straight to the cart, one at a time.** Magic Box is not turned on in the Smootify dashboard. It needs to be on.
> - **The whole box disappears.** `smootify-magic-box-cart` is missing, or `data-handle` does not match the handle of a Magic Box entry in Shopify.
> - **The add button never turns on.** The number of products is not between `data-min` and `data-max`, or not a multiple of `data-step`, or a category is empty with `data-all-categories`.
> - **There is no discount.** The box is static (`data-title`, `data-image`). Only a Magic Box entry carries a discount.
> - **The box image is missing at checkout.** The `data-image` URL is not a Shopify file. Upload it under **Content > Files**.
> - **A rule is on though you wrote `false`.** These rules turn on with any value. Remove the attribute instead.
