# Build the cart

> The cart as a page or as a mini cart that opens over the page. Covers the line template, totals, quantity and remove, the empty state, errors, checkout, and the ways a cart can open.

Status: Ready (Smootify 2.0)  
URL: https://www.smootify.io/docs/astro/build/cart  
Updated: 2026-10-21

The cart is one element, `smootify-cart`, with a `<form>` inside it. The form’s success and error messages are the elements with `form-done` and `form-fail`: see [Write components](https://www.smootify.io/docs/astro/build/components). The same element makes a cart page or a mini cart that opens over the page: only what you wrap the form in changes. This guide builds the cart first, then shows the ways to open it.

Discount codes, notes and progress bars are in [Discounts, notes and progress bars](https://www.smootify.io/docs/astro/build/cart-extras). Product suggestions are in [Cart upsells](https://www.smootify.io/docs/astro/build/cart-upsells).

## How it fits together

What Smootify needs:

- a `smootify-cart` element;
- inside it, a `<form>`. The form holds the line template (`cart-item`) and the checkout button;
- next to the form, `form-done` and `form-fail` elements, for the empty cart and for errors.

What is up to you:

- where the cart lives: a page of its own, the navbar, or both;
- the layout, the order of the parts, the classes and the styles;
- which parts you show. A mini cart can show only the title and total of each line.

The smallest cart looks like this:

```html
<smootify-cart>
  <form>
    <cart-item>
      <img cart-item="image" alt="" />
      <div cart-item="title">Product</div>
      <div cart-item="variant-title">Red / M</div>
      <quantity-input>
        <button type="button" data-action="minus">-</button>
        <input type="number" name="quantity" />
        <button type="button" data-action="plus">+</button>
      </quantity-input>
      <div cart-item="total">$0.00</div>
      <button type="button" data-action="remove">Remove</button>
    </cart-item>
    <div cart="subtotal">$0.00</div>
    <button type="submit">
      <span data-state="default">Check out</span>
      <span data-state="loading">Please wait…</span>
    </button>
  </form>
  <div form-done hidden>Your cart is empty</div>
  <div form-fail hidden>Something went wrong</div>
</smootify-cart>
```

## 1. Add the cart and its form

1. Write a `smootify-cart` element. For a mini cart, put it in the navbar (see [Navbar](https://www.smootify.io/docs/astro/build/navbar)). For a cart page, put it on a page of its own.
2. Inside it, write a `<form>`, and next to the form a `form-done` and a `form-fail` element, both with `hidden`.

Smootify uses the three parts of the cart this way:

- **`<form>`**: the lines, the totals and the checkout button. Hidden while the cart is empty.
- **`form-done`**: the empty cart, shown when there are no lines. It is never a success message.
- **`form-fail`**: Shopify’s errors, written in its text and shown for 5 seconds. Screen readers read them out.

Design `form-done` as the empty cart, and leave a text element inside `form-fail`: Smootify writes each error into it, such as a quantity above the stock. A change that cannot reach Shopify, such as a quantity, a removal, a code, the note or the checkout while the visitor is offline, shows its error there too, and the line comes back from its loading state.

While the cart is empty, `smootify-cart` has the class `is-empty-cart`. You can style it from that class.

The cart uses the first form inside `smootify-cart`. A form inside a product card in the cart, such as an upsell, is left alone.

## 2. Design one line

Add a `cart-item` element inside the form. It is the template of a line: Smootify copies it once for each line in the cart. Design only one. Any other `cart-item` you add is removed.

Inside it, add an attribute to an element to fill it with a value of the line:

| Attribute | Put it on | What it shows |
|---|---|---|
| `cart-item="title"` | text | the product title |
| `cart-item="variant-title"` | text | the variant, such as “Red / M”; hidden for products without variants |
| `cart-item="image"` | an `img` | the variant image (`product-image` for the product’s main image) |
| `cart-item="price"` | text | the price of one item |
| `cart-item="total"` | text | the line total, after the line’s discounts |
| `cart-item="compare-at-price"` | text | the compare-at price; hidden when there is none |
| `cart-item="savings"` | text | what the line saves; hidden when it saves nothing |
| `cart-item="unit-price"` | text | the price per unit, such as €1.50/ 100 g; empty without one |
| `cart-item="url"` | an `a` | links to the product page |

An `a` with `cart-item="url"` and only text inside also shows the product title. Every value is in [cart line values](https://www.smootify.io/docs/astro/reference/attributes/cart-item).

**Options and extra fields.** An element with `cart-item="options"` is repeated once for each option of the variant. Inside it, `option="name"` shows the name (“Size”) and `option="value"` the value (“M”). The same template also lists the extra fields the visitor filled in the add-to-cart form, such as an engraving, and the name of the subscription plan.

**Metafields.** The product’s metafields work inside the line, with the same attributes as on the product page: see [Metafields on the page](https://www.smootify.io/docs/astro/build/metafields).

**While a line changes.** A line shimmers and ignores clicks while its quantity changes or while it is removed. It has the class `updating` or `removing` during that time.

## 3. Quantity and remove

**Quantity.** Inside `cart-item`, add a `quantity-input` element with an `<input name="quantity">` inside it. The minus and plus buttons are optional: they are `<button type="button">` elements with `data-action="minus"` or `data-action="plus"`.

- A change updates the line at once. Going down to 0 removes the line.
- Plus stops at the stock or at the product’s quantity rule, a B2B one or one of the [purchase rules](https://www.smootify.io/docs/astro/features/purchase-rules). `data-disable-max` on `quantity-input` removes that limit.
- With both buttons, the field does not take clicks: the number changes through the buttons. Without buttons, the visitor types the number.
- Some lines cannot change quantity, such as a [Magic Box](https://www.smootify.io/docs/astro/features/magic-box). Their quantity field is removed.

**Remove.** Inside `cart-item`, add a `<button type="button">` with `data-action="remove"`. It removes the line, together with the other lines of its bundle.

## 4. Totals

Add an attribute to a text element anywhere inside `smootify-cart`, inside or outside the form. Here, three items cost €150 at compare-at prices, €120 at their sale prices, and a discount code takes €12 off the order:

| Attribute | What it shows |
|---|---|
| `cart="count"` | the number of items (a line with 3 items counts as 3) |
| `cart="subtotal"` | the amount before the discounts on the whole order |
| `cart="compare-at-total"` | the total at compare-at prices; hidden unless it is higher than the total |
| `cart="savings"` | the compare-at total minus the total; hidden when nothing is saved |
| `cart="total"` | the amount after every discount |

`cart="savings"` replaces what is inside it with the amount, and hides only itself. A label such as “You save” placed next to it stays visible when there is nothing to save.

**Shipping.** For a logged-in customer with an address, Shopify adds a shipping estimate to the total. `cart="shipping"` shows it, so that subtotal, shipping and taxes add up to the total. Without an estimate (no address yet, free shipping, or draft prices) the element is hidden. To show a text instead, such as “Calculated at checkout”, give it the attribute `data-fallback` with that text. `data-fallback` works the same on every `cart` value, and an empty one is ignored.

Taxes, and shipping without an address, are calculated at checkout, so the total in the cart is not always the final amount. The discount and gift card lists are in [Discounts, notes and progress bars](https://www.smootify.io/docs/astro/build/cart-extras). Every value is in [cart values](https://www.smootify.io/docs/astro/reference/attributes/cart).

`skeleton="text"` on a total shows a shimmer until the cart loads. See [loading](https://www.smootify.io/docs/astro/reference/attributes/loading).

## 5. Checkout

The checkout button is the form’s submit button, `<button type="submit">`. A click sends the visitor to Shopify’s checkout. Before that, Smootify saves the cart note and the other fields of the cart form (see [cart attributes](https://www.smootify.io/docs/astro/build/cart-extras)). Inside it, `[data-state="default"]` holds the label and `[data-state="loading"]` what shows while this happens.

- **Shop Pay.** A `smootify-shop-pay` element inside `smootify-cart` shows Shopify’s Shop Pay button for the cart. Inside `smootify-product` it shows the button for the chosen variant. Shopify draws the button, and it always fills the element: for a narrower button, give the element a width, not a background or a centred layout.
- **Checkout domain.** The checkout is Shopify’s page and opens on the store’s primary domain, set in Shopify under **Settings, Domains**. The domain of your site is a separate step: see [Deploy to Cloudflare](https://www.smootify.io/docs/astro/start/deploy#3-add-your-domain).
- **A quote instead of checkout.** See [Quotes and B2B](https://www.smootify.io/docs/astro/features/quotes).

**A link to Shopify’s own cart.** Some Shopify apps work only on the cart page of Shopify’s online store, such as pickup and delivery schedulers like Pickeasy. For them, add the attribute `shopify-link="cart"` to an `a` link, on any page. Smootify points the link to the online store’s cart, on your store’s `myshopify.com` address, with the visitor’s lines and their extra fields, and updates it every time the cart changes. When more than one line has extra fields or is a bundle, the link opens the visitor’s cart itself instead. Any other value is a path of the store’s address: `shopify-link="account"` opens `/account` there.

## 6. A page or a mini cart

A `smootify-cart` with nothing around its form is a cart page: it shows in the page. To make a mini cart, wrap the form in one of these, inside `smootify-cart`:

| Way | Build it with | Opens with | Good to know |
|---|---|---|---|
| A cart page | nothing: the `<form>` sits right inside `smootify-cart` | always shown, on a page of its own | open and close buttons do nothing here |
| Popover | an element with the attribute `popover` | its `popovertarget` button, or an open-cart button | stays open while the visitor types |
| Dialog | a `dialog` element; its first child is the panel | an open-cart button | a click on the backdrop and Escape close it |
| Details | a `details` element, with a `summary` | the `summary` | a click outside closes it |

You don’t tell Smootify which one you use. It looks inside `smootify-cart`, in this order, for a popover, a dialog and a details. The first it finds decides how the cart opens. Use only one.

**Open and close buttons.** A `<button>` with `data-action="open-cart"` opens the cart; `data-action="close-cart"` closes it (`open` and `close` also work). Both must be inside the same `smootify-cart`. Keep them outside the form, or give them `type="button"`: inside the form, a button without it also submits the cart and goes to checkout.

**Open after an add.** Add `data-open="on-add"` to `smootify-cart`. The mini cart then opens each time a product is added, except with Buy now, which goes straight to checkout. On a page that has another `smootify-cart` with nothing around its form, such as your cart page, `data-open` does nothing.

For developers, [`openCart`](https://www.smootify.io/docs/astro/reference/api/methods/opencart) and [`closeCart`](https://www.smootify.io/docs/astro/reference/api/methods/closecart) open and close it from code, and the [`cart_opened`](https://www.smootify.io/docs/astro/reference/api/events/cart-opened) and [`cart_closed`](https://www.smootify.io/docs/astro/reference/api/events/cart-closed) events tell when it happens.

### In a dialog

A dialog is a panel over the page, such as a drawer. Put a `dialog` element inside `smootify-cart`. Its first child is the panel: design it as you like, such as a column fixed to the right. Everything around the panel is the backdrop.

```html
<smootify-cart>
  <button type="button" data-action="open-cart">Cart</button>
  <dialog>
    <div>
      <button type="button" data-action="close-cart">Close</button>
      <form>
        <cart-item><div cart-item="title">Product</div><div cart-item="total">$0.00</div></cart-item>
        <button type="submit">Check out</button>
      </form>
      <div form-done hidden>Your cart is empty</div>
      <div form-fail hidden>Something went wrong</div>
    </div>
  </dialog>
</smootify-cart>
```

- A click on the backdrop and the Escape key close it.
- The first child of the dialog fades and scales in (opacity 0 to 1, scale 0.9 to 1, in 0.5 seconds) and out (opacity 1 to 0, scale 1 to 0.85, in 0.3 seconds). To change it, set `window.cartInAnimation`, `window.cartOutAnimation`, `window.cartInAnimationSettings` and `window.cartOutAnimationSettings` in an inline script that runs before Smootify starts: see [Options](https://www.smootify.io/docs/astro/developers/options). From CSS you style the dialog box and the backdrop.
- The backdrop is 80% black. The CSS variable `--smootify-backdrop-color` changes it: see [CSS](https://www.smootify.io/docs/astro/developers/css).

### In a details element

Put a `details` element inside `smootify-cart`. Inside it, a `summary` element is the trigger, and can hold the count. The element after it is the panel.

A click on the summary opens and closes the cart, and a click outside the details closes it. A close button goes inside the panel.

### In a popover

A popover opens above the page. Escape and a click outside close it, and the browser handles the focus. Use an element with the attribute `popover`.

```html
<smootify-cart data-open="on-add">
  <button type="button" data-action="open-cart">Cart <span cart="count">0</span></button>
  <div popover="auto">
    <button type="button" data-action="close-cart">Close</button>
    <form>
      <cart-item><div cart-item="title">Product</div><div cart-item="total">$0.00</div></cart-item>
      <button type="submit">Check out</button>
    </form>
    <div form-done hidden>Your cart is empty</div>
    <div form-fail hidden>Something went wrong</div>
  </div>
</smootify-cart>
```

- Open it with a `data-action="open-cart"` button as above, or with a button whose `popovertarget` is the panel’s `id`.
- The popover stays open while the visitor types in the cart’s fields and while the lines change.
- To animate the panel, use CSS transitions (`@starting-style` and `transition-behavior: allow-discrete`): the animation plays when Smootify opens the cart after an add to cart too.
- Unless the cart is in a dialog or a details, a popover anywhere inside `smootify-cart`, even in a line, makes Smootify open the cart as a popover. Keep other popovers out of the cart.

More in [Popovers](https://www.smootify.io/docs/astro/build/popovers).

## Before you publish

In the dev server and on your `*.workers.dev` address, Smootify has a debugger. Its panel lists the elements that are out of place on the page. Open it with Alt+Shift+D, or add `?smootify-debug` to the address.

> [!STUCK]
> - **Only one line shows, or lines you designed disappear.** Only the first `cart-item` is the template. Design one.
> - **The quantity buttons do nothing.** `quantity-input` is outside the form, or its buttons are links. Use `<button type="button">`.
> - **A close button goes to checkout.** It is inside the form without `type="button"`.
> - **The mini cart does not open after an add.** `data-open` is missing, or the page has another `smootify-cart` with nothing around its form, such as the cart page.
> - **The compare-at total is missing.** It shows only when it is higher than the total: nothing in the cart is on sale or discounted.
