# Build the product page

> The Shopify product on the template page of your Products collection. Covers title, price, gallery, options, add to cart and stock, and says what Smootify needs and what is up to your design.

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

A product page in Webflow is the template page of your **Products** collection. Smootify loads the live product from Shopify into it: price, options, stock, images and the add-to-cart form. This guide goes through the page one part at a time. For each part it says what Smootify needs and what is up to your design.

> [!NOTE]
> Before you start, Smootify must be installed on the site and your products synced to the **Products** collection, with the `cms:Shopify ID` field filled in.

## How it fits together

Everything Smootify fills goes inside one element, `smootify-product`. It holds one product, and the elements inside it read that product and no other.

What Smootify needs:

- a `smootify-product` element with the product’s id;
- inside it, the add-to-cart form, if the page sells.

What is up to you:

- the layout, the sections, the order of the parts, the classes and the styles;
- which parts you use. A page can show a price without a gallery, or options without a quantity field.

The smallest page that sells looks like this in the Navigator:

```html
<smootify-product data-id="{Shopify ID}">
  <h1 product="title">{Name}</h1>
  <smootify-price><div data-prop="price">{Price Formatted}</div></smootify-price>
  <smootify-add-to-cart>
    <div class="w-form">
      <form><input type="submit" value="Add to cart" data-wait="Adding…"></form>
      <div class="w-form-done"><div>Added</div></div>
      <div class="w-form-fail"><div>Something went wrong</div></div>
    </div>
  </smootify-add-to-cart>
</smootify-product>
```

A **Custom element** comes from the Add panel. You set its tag in the element settings, and its attributes under **Custom attributes** in the same panel.

## 1. Connect the page to the product

1. On the template page, add a Custom element with the tag `smootify-product`. It can wrap the whole page or only the product section.
2. Give it the custom attribute `data-id`, and take its value from the **Shopify ID** field of the collection.

Everything else on this page goes inside that element.

- **Shared links to a variant.** A link to the page with `?variant=` and a variant id opens that variant. An id that is not one of the product’s variants, such as an old link to a variant you deleted, is ignored: the page opens on the first available variant. As the visitor picks options, the address follows the variant, so the link can be shared. This only works for the page’s own product: a `smootify-product` inside a Collection list, or inside the cart, ignores it.
- **Products Shopify does not return.** A product that is unpublished or not sold in the visitor’s market leaves the product block empty, marked with the class `w-dyn-bind-empty`. Smootify does not redirect.
- **Other products on the page.** The same element also loads related products, best sellers or a collection: see the `data-id` values in [`smootify-product`](https://www.smootify.io/docs/reference/elements/smootify-product).

## 2. Title, description and product details

Add a custom attribute to a Webflow element to fill it with a value of the product. What you type in the Designer stays as a placeholder until the product loads.

| Attribute | Put it on | What it shows |
|---|---|---|
| `product="title"` | a Heading or text | the title |
| `product="description"` | a Rich text element | the description, with its formatting |
| `product="vendor"` | text | the vendor (brand) |
| `product="type"`, `product="category"` | text | the product type, the category |
| `product="tags"` | text | one copy per tag |
| `product="collections"` | text or a link | one copy per collection |
| `variant="sku"`, `variant="barcode"`, `variant="weight"` | text | values of the selected variant |

> [!TIP]
> Also bind the element's text to the CMS field, such as the `cms:Name` field for the title. The published page then has the text before Smootify loads, which helps search engines and avoids a flash of placeholders. Smootify then writes the live value from Shopify over it.

The full lists are in the Reference: [product values](https://www.smootify.io/docs/reference/attributes/product) and [variant values](https://www.smootify.io/docs/reference/attributes/variant).

### Metafields

`metafield="material"` shows the product metafield `custom.material`. Add `namespace="…"` when the namespace is not `custom`, and use `variant-metafield` for a variant’s metafield. Every metafield type renders, including lists, ratings, JSON and references to other products: see [metafield types](https://www.smootify.io/docs/reference/metafield-types).

In Shopify, the metafield definition must have **Storefront access** turned on. Without it the value never reaches the page.

## 3. Price

Add a Custom element with the tag `smootify-price`. Inside it, the elements with `data-prop` get the amounts, in the store’s money format:

- `data-prop="price"`: the price of the selected variant;
- `data-prop="compareAtPrice"`: the compare-at price, hidden when the variant is not on sale;
- `data-prop="total"`: the price times the quantity in the form.

The price follows the variant, the quantity and the selling plan. Put it anywhere inside `smootify-product`. Inside a swatch button, it shows the price of that button’s value instead.

```html
<smootify-price skeleton="text">
  <div data-prop="price">{Price Formatted}</div>
  <div data-prop="compareAtPrice">{Compare at Price Formatted}</div>
</smootify-price>
<div condition="on-sale"><span variant="discount-percentage">20%</span> off</div>
```

Attributes on `smootify-price` change what it shows:

- `no-quantity`: the total ignores the quantity;
- `data-ignore="subscription-swatches"`: the one-time price, whatever selling plan is chosen;
- `data-remove="…"`: removes that text from the formatted price;
- `data-replace="from|to"`: replaces one piece of text with another.

`variant="discount-percentage"` shows `0%` on a product that is not on sale. Put it inside an element with `condition="on-sale"`, as above, so the badge only shows during a sale.

`condition="not-on-sale"` is the opposite: shown while the chosen variant is at full price, for a “regular price” label or a price style without the sale colour.

`variant="discounted-amount"` shows how much the variant saves, such as “€8.00”. Like the percentage, it comes only from the compare-at price: discount codes and automatic discounts never change it. The cart shows those. It shows `€0.00` at full price, so put it inside `condition="on-sale"` too.

**Price per unit.** For products sold by weight, volume or length, the European pricing rules ask for the price per unit next to the price. Add `variant="unit-price"` to a text element: it shows the selected variant’s unit price and its reference unit, such as `€1.50/ 100 g`. The unit is in a span with the class `reference-unit`, so you can style it apart. `data-format="{{price}} per {{unit}}"` writes it your way. The element stays empty when the variant has no unit price.

The unit price comes from Shopify. On the product, in the pricing of each variant, fill in the unit price measurement: the variant’s total amount (such as 500 g) and the base unit to price it by (such as 100 g). Shopify then works out the price per unit. Shopify offers this setting to stores in some countries only.

**Where the price format comes from.** Smootify writes every amount with the store’s money format: the **HTML without currency** field of Shopify’s currency formatting (**Settings › General**, then **Change currency formatting**). Smootify keeps the format in the visitor’s browser and refreshes it in the background, so after a change, check it in a private window. The `useBrowserFormatter` [option](https://www.smootify.io/docs/reference/options) formats prices for each market with the browser’s own rules instead.

A sale that shows in one country and not in another can be Shopify hiding converted compare-at prices from European countries: see [Markets](https://www.smootify.io/docs/features/markets#what-changes-with-the-country).

## 4. Gallery

Add a Div block with `product="gallery"` and put the parts you need inside it. Every part is optional.

- **`gallery="slider"`** goes on a Webflow **Slider**. Smootify makes one slide per image, each a copy of the first slide. Put an Image in the first slide, or use the slide’s background. The other slides you design are removed.
- **`gallery="thumbnails"`** goes on the element that repeats once per image, such as a Link block with an Image inside. Design one, and Smootify repeats it. The current one gets the class `is-active`, and a click moves the slider.
- **`gallery="lightbox"`** goes on a Lightbox link that opens every image. The link keeps what you put in it, such as a zoom icon.
- **`gallery="lightboxes"`** goes on a Lightbox link that repeats once per image, with its picture.

```html
<div product="gallery" include-media="">
  <div class="w-slider" gallery="slider"><div class="w-slider-mask"><div class="w-slide"><img src="{Image}" alt="{Image Alt}"></div></div></div>
  <div><a href="#" class="w-inline-block" gallery="thumbnails"><img src="{Image}" alt="{Image Alt}"></a></div>
  <a href="#" class="w-inline-block w-lightbox" gallery="lightbox"><img></a>
</div>
```

In the image settings, bind the Image of the first slide and of the thumbnail to the **Image** field, and their Alt text to **Image Alt**. If your Products collection uses the **Image URL** field, where the image stays on Shopify, bind the image to that field instead, with the same **Image Alt**. The page then shows the featured image before Smootify loads, and Smootify replaces it with every image of the product.

Attributes on the `product="gallery"` element:

- **`include-media`** adds videos, YouTube and Vimeo videos and 3D models. Without it, the gallery shows images only.
- **`only-variant-images`** shows only the images of the variant the visitor picks: only the red t-shirt when they pick Red. How Smootify knows which images are the red ones is just below.
- **`variant-images="alt"`**, together with `only-variant-images`, groups the images by their alt text instead. The tag after the last `#` names the variant: `Oak chair #Oak`, `# Red / M`. An image without a tag shows for every variant. The alt text is public, since screen readers and Google read it, so write it to make sense read aloud.

**Which images belong to which variant.** In Shopify, each variant can have one image of its own, chosen on the variant’s page. That image is where the variant’s group starts: the group takes it and the images after it, in their Shopify order, up to the next image chosen for another variant. Photos placed before the first of these images belong to no variant, so the gallery never shows them once a variant is picked. Pick a colour and switch between the two ways:

Example: a t-shirt in Red, Blue and Green with eight images in this order: 1 a photo of the model (no color), 2-4 Red (2 is the image of the Red variant), 5-6 Blue (5 is the image of Blue), 7-8 Green (7 is the image of Green). By position, Red shows 2, 3, 4; Blue 5, 6; Green 7, 8; image 1 is never shown. By alt text (2-8 end with "# Red", "# Blue", "# Green", 1 has no tag), Red shows 1, 2, 3, 4.

To show a photo with every colour, such as the photo of the model, group the images by alt text (`variant-images="alt"`): an image without `#` in its alt text shows with every variant.

When the variant changes, the slider moves to its image. Clicking an image does not change the variant. More in [gallery parts](https://www.smootify.io/docs/reference/attributes/gallery).

## 5. Options

A picker shows the values of one option, such as Colour or Size, and changes the variant. There are four, each built on a different Webflow element:

| Picker | Built with | Inside |
|---|---|---|
| `variant-swatches` | a Custom element | one Custom element with the tag `button`: the template of every value |
| `variant-dropdown` | a Custom element around a Webflow Dropdown | one Dropdown link: the template of every value |
| `variant-popover` | a Custom element with a popover inside | links or buttons inside the popover element |
| `variant-selector` | a Custom element around a Select field | the Select field: Smootify writes its options |

A picker goes anywhere inside `smootify-product`, inside or outside the add-to-cart form. You can use more than one kind on the same page, such as swatches for Colour and a dropdown for Size.

**Design three pickers.** A Shopify product has up to three options, such as Colour, Size and Material, and one template page serves every product. So give the page three pickers, one per option: `data-option="1"`, `"2"` and `"3"`. A picker removes itself when the product does not have that option: a product with one option shows one picker, a product with three shows all three, a product without options shows none.

**Which option.** Use `data-option="1"` for the first option, `2` for the second, or `data-option-name="Color,Colour"` for the first of those names that the product has, written exactly as in Shopify. The name is useful when products name their options differently, or in more than one language. Without either attribute, the picker shows the first option.

**Design only the first value.** The first button, or the first link, is the template. Smootify copies it for every value and removes the others you designed, so every attribute goes on the first one.

```html
<smootify-add-to-cart>
  <div class="w-form">
    <form>
      <div has-option="0">
        <div><span variant="option1-name">Option</span>: <span variant="option1-value">Value</span></div>
        <variant-swatches data-option="1"><button><span option="bg-color"></span><span option="title">Value</span><span option="price-difference">+ $0</span></button></variant-swatches>
      </div>
      <div has-option="1">
        <div><span variant="option2-name">Option</span>: <span variant="option2-value">Value</span></div>
        <variant-swatches data-option="2"><button><span option="title">Value</span></button></variant-swatches>
      </div>
      <div has-option="2">
        <div><span variant="option3-name">Option</span>: <span variant="option3-value">Value</span></div>
        <variant-swatches data-option="3"><button><span option="title">Value</span></button></variant-swatches>
      </div>
      <input type="submit" value="Add to cart" data-wait="Adding…">
    </form>
  </div>
</smootify-add-to-cart>
```

Each group holds the option’s name (`variant="option1-name"`), the value picked (`variant="option1-value"`) and the picker. The group’s Div block has `has-option`, so it goes away together with its picker when the product does not have that option. Here the count starts at 0: `has-option="0"` is the first option, `"1"` the second, `"2"` the third. `has-option` works inside the add-to-cart form.

Inside the value of swatches, dropdowns and popovers:

- `option="title"`: the value’s name;
- `option="bg-color"` or `option="color"`: the swatch colour, as a background or as the text colour;
- `option="image"`: the swatch image;
- `option="price-difference"`: how much more the value costs than the cheapest one, such as ”+ €180”. It is hidden on the cheapest value, and `data-format="+ {{price}}"` changes the text.

Swatch colours and images come from Shopify: the option’s own swatches, or the option metaobject in the product metafield `custom.options`. A native select can only hold text. On `variant-selector`, `data-format="{{value}} {{difference}}"` on the Select field adds the difference to each option’s text.

The selected value gets the class `is-active`. A value that cannot be bought gets `is-disabled` but stays clickable, because a sold-out variant can still be chosen. Style both with combo classes.

**Show the option and the chosen value.** Inside the picker, `selected-option="name"` writes the option’s name and `selected-option="title"` the chosen value, for example in a label or a dropdown toggle that reads “Size: M”. The name comes from the picker’s `data-option-name` or `data-option`, so the label never needs to be typed again:

```html
<variant-swatches data-option-name="Size">
  <div><span selected-option="name">Size</span>: <span selected-option="title">M</span></div>
  <button><span option="title">S</span></button>
</variant-swatches>
```

Anywhere else in `smootify-product`, use `variant="option1-value"` (or `option2-value`, `option3-value`) for the value and `variant="option1-name"` for the name, as in the groups above.

Two attributes on `smootify-add-to-cart` change how pickers behave. `allow-unavailable` lets the native select pick values that cannot be bought. `avoid-option-update` keeps the value picked in a dropdown or popover when the variant changes from another picker.

The page’s product loads all its variants, even above 250.

## 6. Add to cart

Add a Custom element with the tag `smootify-add-to-cart` and put a Webflow **Form Block** inside it. The form is required: without one, the element is removed.

```html
<smootify-add-to-cart>
  <div class="w-form">
    <form>
      <quantity-input><button data-action="minus">-</button><input name="quantity"><button data-action="plus">+</button></quantity-input>
      <button type="submit"><div data-state="default">Add to cart</div><div data-state="loading">Adding…</div></button>
      <button type="submit" name="buy-now">Buy now</button>
    </form>
    <div class="w-form-done"><div>Added</div></div>
    <div class="w-form-fail"><div>Something went wrong</div></div>
  </div>
</smootify-add-to-cart>
```

**The buttons.** The add button can be either of the first two kinds. Buy now is optional, a second button next to it:

- A Webflow Form Button: text only; its Waiting text setting shows while the product is added.
- A Custom element with the tag `button` and `type="submit"`: anything inside; `data-state="default"` and `data-state="loading"` elements swap while adding.
- Buy now: a second `button` with `type="submit"` and `name="buy-now"` adds and goes to checkout (not a Webflow Button, which is a link).

**Quantity.** Add a Custom element with the tag `quantity-input`. Put a Text field named `quantity` inside it. The minus and plus buttons are optional: they are Custom elements with the tag `button` and `data-action="minus"` or `data-action="plus"`. Without a quantity field, the product is added once. The minus button is disabled and gets `is-disabled` at the lowest quantity, and the plus button at the highest: the stock left, or the maximum of a B2B quantity rule. `data-disable-max` on `quantity-input` removes that upper limit.

**Extra fields.** Any other field in the form, such as a text field, a select or a checkbox, is saved on the cart line. Its field name is the label, and it shows at checkout: use it for an engraving or a gift message. A name that starts with `_` is saved but hidden at checkout. A name that starts with `##ignore` is not saved. Extra fields never change the price: for options that cost more, use [add-ons](https://www.smootify.io/docs/features/add-ons) or the [product configurator](https://www.smootify.io/docs/features/product-configurator).

**When the variant cannot be bought.** If the variant is sold out, or the picked combination does not exist, every submit button in the form is disabled and gets the class `is-disabled`. Shopify’s errors, such as a quantity above the stock, appear in the form’s Error message. The Success message is never shown: the form stays on screen, ready for the next product.

**A button outside the form.** A sticky bar with its own Add to cart button can sit anywhere on the page. Give the Form an ID in its settings, such as `product-form`, and make the button a Custom element with the tag `button`, the custom attribute `type` set to `submit` and `form` set to that ID. It adds to the cart, shows its loading label and is disabled with the others. The ID must be unique on the page, so use it on the product page, not in a list of cards.

## 7. Stock, sale and other states

An element with `condition="…"` inside `smootify-product` shows only while the product is in that state. The element stays on the page and Smootify shows or hides it. This is separate from Webflow’s conditional visibility. Each card shows a value of `condition` and the kind of message you would put in that element:

| Condition | Shown while the selected variant |
|---|---|
| `in-stock` / `out-of-stock` | can / cannot be bought |
| `low-stock` | has 5 or fewer left (0 counts too) |
| `low-not-last-stock` | has 2 to 5 left |
| `last-in-stock` | has one left |
| `many-in-stock` | has more than 5 left |
| `in-backorder` | is out of stock but still sold |
| `on-sale` | has a compare-at price above its price |
| `disabled` | is a combination that does not exist |
| `is-new` | belongs to a product published less than 30 days ago |
| `subscription` / `no-subscription` | has a selling plan chosen, or not |

Every condition is in [product conditions](https://www.smootify.io/docs/reference/conditions/product).

- **Variants.** `condition="is-variable"` shows an element when the product has more than one variant, such as the options block, and `is-not-variable` when it has only one.
- **Subscriptions.** `condition="subscription"` follows the visitor’s choice: it shows while a selling plan is chosen in `subscription-swatches`, and `no-subscription` while the one-time purchase is. It does not tell whether the product offers subscriptions.
- **Tags.** `tagged="gift,new"` keeps an element only when the product has every one of those tags. Add `any="true"` to keep it when the product has at least one. `not-tagged` does the opposite: see [tag conditions](https://www.smootify.io/docs/reference/conditions/product-tags).

`smootify-product` also carries classes for the same states, such as `is-available`, `is-on-sale`, `low-stock`, `is-variable` and `loaded`. You can style them as combo classes: see [state classes](https://www.smootify.io/docs/reference/state-classes).

**When the product is not available.** Shopify does not return a product that is not published on the Headless channel, or not sold in the visitor’s market. The page’s `smootify-product` is then hidden, and the page gets the class `product-unavailable`. Two conditions work anywhere on the page, outside `smootify-product` too:

- `condition="product-unavailable"` shows an element only in that case, such as a message with a country switcher;
- `condition="product-available"` does the opposite: it hides an element in that case.

```html
<div condition="product-unavailable">
  <div>This product is not available in your country.</div>
  <country-switcher>
    <div class="w-dropdown">
      <div class="w-dropdown-toggle"><div data-prop="name">Country</div></div>
      <nav class="w-dropdown-list"><a href="#" class="w-dropdown-link"><div data-prop="name">Country</div></a></nav>
    </div>
  </country-switcher>
</div>
```

When the visitor picks a country where the product is sold, the class goes away, the message hides and the product shows. Only the page’s own product counts: cards in Collection lists, cart lines and products inside other products are hidden as before, and do not turn the condition on.

**While it loads.** `skeleton="text"`, `"button"`, `"block"` or `"image"` shows a placeholder shimmer on an element until Smootify fills it, and `skeleton="hide"` hides it until then. Only elements that have the attribute shimmer. See [loading](https://www.smootify.io/docs/reference/attributes/loading).

## 8. Subscriptions

To sell by subscription, the store needs a Shopify subscription app that creates the selling plans. Smootify shows and sells them.

Put a Custom element with the tag `subscription-swatches` inside the form, with a Webflow **Tabs** element in it:

- the **first tab** is the one-time purchase, and is removed when the product can only be bought by subscription;
- the **second tab** is the template for each selling plan group. In its pane, a Radio button is the template for each plan;
- `subscription-group="name"` shows the group’s name in the tab. A `smootify-price` inside a plan’s Radio button shows that plan’s price.

```html
<subscription-swatches>
  <div class="w-tabs">
    <div class="w-tab-menu"><a class="w-inline-block w-tab-link"><div>One-time purchase</div></a><a class="w-inline-block w-tab-link"><div subscription-group="name">Subscribe</div></a></div>
    <div class="w-tab-content">
      <div class="w-tab-pane"><div>Pay once</div></div>
      <div class="w-tab-pane"><label class="w-radio"><input type="radio" class="w-radio-input"><span class="w-form-label">Plan</span><smootify-price><span data-prop="price">$0.00</span></smootify-price></label></div>
    </div>
  </div>
</subscription-swatches>
```

On a product without selling plans, `subscription-swatches` is removed. The attributes that preselect a plan are in [`subscription-swatches`](https://www.smootify.io/docs/reference/elements/subscription-swatches).

## 9. Search engines

Smootify adds the product’s structured data (JSON-LD) to the page, so Google can show the product as a rich result. It includes:

- the name, description, images and brand;
- for each variant, the price in the visitor’s currency and whether it is available;
- the rating, when the page shows the `reviews.rating` and `reviews.rating_count` metafields (Google wants a marked-up rating to be visible on the page).

There is nothing to add. If the page already has its own Product structured data, Smootify leaves it alone. To turn it off, set `structuredData: false` in the [Smootify options](https://www.smootify.io/docs/reference/options).

For Google Shopping, the product feed of Shopify’s Google & YouTube app is still the reference.

## Before you publish

On your `.webflow.io` address, press Alt+Shift+D (Option+Shift+D on a Mac) to turn on Smootify’s debugger. On a phone, open the page with `?smootify-debug` at the end of the address. Its button then shows on every page of the site, and its panel lists the elements that are out of place. [The debugger](https://www.smootify.io/docs/start/debugger) explains the rest.

> [!STUCK]
> - **Nothing loads.** `data-id` is empty or not bound to the `cms:Shopify ID` field.
> - **Attributes on a swatch do nothing.** They are on the second button: only the first one is the template.
> - **A picker disappears.** The product does not have that option, or the picker is inside an element hidden by Webflow's conditional visibility, which removes it.
> - **A metafield stays empty.** Its definition in Shopify needs Storefront access.
> - **Videos are missing from the gallery.** Add `include-media` to the `product="gallery"` element.
> - **The gallery does not move.** `gallery="slider"` only works on a Webflow Slider.
> - **Buy now does nothing.** It is a Webflow Button, which is a link. Use a Custom element with the tag `button`.
