# Filters, sorting and pagination

> Search & discovery lists products live from Shopify, with the filters of Shopify's Search & Discovery app, sorting and pagination designed in Webflow. Covers the block, every filter type, sorting, the count and active filters, pagination with page numbers and links that keep the filters.

Status: Ready (Smootify 2.0)  
Add-on: Search & discovery  
1.x page: https://docs.smootify.io/docs/search-filters/search-and-discovery  
URL: https://www.smootify.io/docs/build/filters  
Updated: 2026-10-10

Search & discovery lists products straight from Shopify and lets the visitor filter, sort and page through them. The filters are the ones you set up in Shopify’s Search & Discovery app, such as colour, size, availability or price. You design each filter, the sort menu, the product card and the pagination in Webflow, and Smootify fills them.

> [!NOTE]
> Before you start:
> - install Shopify's free **Search & Discovery** app and create your filters in it (**Filters**, then **Add filter**). Note the **label** of each filter: you will type it in Webflow;
> - turn on the **Search & discovery** add-on in the Smootify dashboard. The Lite and Server plans include it. On your `.webflow.io` address it works without it, so you can build and test first.

## How it fits together

What Smootify needs:

- a `smootify-search-discovery` element;
- inside it, one product card: a `smootify-product` element. Smootify repeats it once per product. Without a card, the block is removed.

What is up to you:

- the layout: where the filters, the sort menu, the count and the pagination sit, and how they look;
- which filters you show, and with which kind of element: checkboxes, a dropdown, swatches…;
- whether the filters sit inside the block or in a panel elsewhere on the page.

```html
<smootify-search-discovery limit="24">
  <div filter="count">0</div>
  <div>
    <smootify-product data-id="filter">
      <a product="url" href="#">Product name</a>
      <smootify-price><div data-prop="price">$0.00</div></smootify-price>
    </smootify-product>
  </div>
</smootify-search-discovery>
```

## 1. Add the block and the card

1. Add a Custom element with the tag `smootify-search-discovery`.
2. Inside it, add a Div block for the grid, and inside that a `smootify-product` with `data-id="filter"`. Design it as a [product card](https://www.smootify.io/docs/build/product-cards).

The first `smootify-product` is the template: each product of the results gets a copy of it. So every element of the card needs a Smootify attribute, such as `product="title"` or `variant="image"`. Text bound only to a CMS field would show the first product’s text on every card.

**A Collection list inside.** You can use a Collection list of product cards as the grid. Its first Collection item becomes the template, and the other items stay on the page until the first results replace them. Search engines then see real products, and the page keeps its height while it loads. The cards can keep `data-id` bound to the **Shopify ID** field, or use `data-id="loader"`: Smootify sets them to `filter` when the page loads. See [placeholder cards](https://www.smootify.io/docs/build/product-cards).

**Which products.** Without other settings, the block lists the whole catalog. Three attributes narrow it, and the visitor cannot remove them, not even with the reset:

- `data-collection` with the collection’s **Shopify ID**: the products of one collection. Use it on the [collection page](https://www.smootify.io/docs/build/collection-page).
- `data-vendor` with the vendor’s name, as written in Shopify: the products of one vendor. Use it on the [vendor page](https://www.smootify.io/docs/build/vendor-page). It needs the **Vendor** filter turned on in Shopify’s Search & Discovery app, as `data-tag` needs the Tag filter.
- `data-tag` with a product tag, such as `data-tag="summer"`: the products with that tag. Next to `data-collection` it works differently: see [Search inside a collection](#search-inside-a-collection).

**How many per page.** `limit` sets the number of products per page: 50 by default, 250 at most. Each card is a full product, so a lower number, such as 24, keeps the page fast, especially on phones.

When a filter picks an option value, such as Colour: Red, each card shows its variant with that value, with its image and price.

## 2. Add the filters

Each filter element shows one filter of Shopify’s Search & Discovery app. Put it inside `smootify-search-discovery` and give it the custom attribute `label` with the filter’s label, written exactly as in the app. Pick the element by how the filter should look:

| Element | Built with | Label | The visitor picks |
|---|---|---|---|
| `filter-checkbox` | a Form Block with one Checkbox | `label="Color"` | several values |
| `filter-list` | a Form Block with one Radio button | `label="Size"` | one value |
| `filter-select` | a Form Block with a Select field | `label="Material"` | one value |
| `filter-dropdown` | a Form Block and a Webflow Dropdown | `label="Material"` | one value |
| `filter-popover` | a Form Block and a popover with links or buttons | `label="Material"` | one value |
| `filter-swatches` | a Form Block and one Custom element with the tag `button` | `label="Color"` | one value, or several with `multiple="true"` |
| `filter-price` | a Form Block with two number fields and two sliders | none: it reads no label | a price range |
| `filter-search` | a Form Block with a Text field | none: it reads no label | words to search |

Without its **Form Block**, or without a `label` where it needs one, the element is removed.

```html
<filter-checkbox label="Color" show-count="">
  <div>Colour</div>
  <div class="w-form">
    <form>
      <label class="w-checkbox"><input type="checkbox" class="w-checkbox-input"><span class="w-form-label">Value</span></label>
    </form>
  </div>
</filter-checkbox>
```

**Design one value only.** The first Checkbox, Radio button, Dropdown link or button is the template, and Smootify copies it for every value of the filter. In a Select field, the first option stays as the placeholder, and Smootify writes the values after it.

**Choices apply at once.** A change reloads the products. For a panel that applies on a click, see the apply button in step 6.

Attributes on each filter element:

- `show-count` adds the number of products next to each value, such as “Red (12)”; add `hide-parenthesis` for “Red 12”. A value with no products gets the class `empty-count`.
- `selected-value="Red"` selects that value when the page opens. On `filter-checkbox`, list several with commas. The visitor cannot remove a preselected value unless you add `allow-reset="true"`.
- `custom-label="Colour"` changes the name shown in the active filters (see step 4).

A filter whose values are all gone hides itself. On `smootify-search-discovery`, `hide-empty` also hides the values with no products, and `hide-if-single` hides a filter that has only one value.

**What the app decides.** Each filter’s settings in Shopify’s Search & Discovery app apply on the site too. Values you group in the app show as one value, and the values keep the order you set there: Smootify does not sort them again. The behaviour you choose for a filter, such as whether two values combine with AND or OR, is applied by Shopify to the results. Give each filter of the app one element: to show the filters in a mobile panel, move them there with `data-expand` (step 6) rather than making a second copy.

**Languages.** Shopify translates the filter values, with these rules of its own:

- the values of the **Vendor** and **Tag** filters are never translated: vendors always show in the store’s default language;
- the **Tag** filter shows only to visitors browsing in the store’s default language;
- translations follow the languages published in Shopify, not your markets.

### Swatches

Inside the template button of `filter-swatches`, `swatch="bg-color"` paints the value’s colour as a background, `swatch="color"` as the text colour, `swatch="image"` shows its image, and `swatch="title"` its name. The colour or image is the value’s swatch in Shopify’s filter: see [Swatch colours and images](https://www.smootify.io/docs/build/swatches). The chosen button gets the class `is-active`. More in [filter swatch parts](https://www.smootify.io/docs/reference/attributes/filter-swatch).

`filter-swatches` keeps only its first button. Put any other button, such as an apply button, outside it.

### Price

`filter-price` is always the price filter, so it needs no `label`. Inside it:

- a Form Block with two Text fields of Type **Number**, for the minimum and the maximum, and two range sliders. Webflow’s form fields have no range type: add the two sliders with a Code Embed inside the form;
- an element with `filter-price="progress"`: the bar between the two handles;
- optionally, `filter-price="from-label"` and `filter-price="to-label"` on text, for the selected minimum and maximum.

```html
<filter-price>
  <div class="w-form">
    <form>
      <input type="number"><input type="number">
      <input type="range"><input type="range">
    </form>
  </div>
  <div><div filter-price="progress"></div></div>
  <div><span filter-price="from-label">$0</span> – <span filter-price="to-label">$100</span></div>
</filter-price>
```

It needs exactly two number fields and two sliders: see [price filter parts](https://www.smootify.io/docs/reference/attributes/filter-price). The range starts from the prices of the first results. Shopify shows the price filter only in the shop’s default currency.

**Style the sliders in the Code Embed.** Smootify adds no style to them: write their CSS in the same Code Embed, for example the size and colour of the handles. Smootify places `filter-price="progress"` with its left and right edges, as a percentage of the track, so give it an absolute position inside a parent with a relative position.

### Collection

`label="Collection"` makes a filter of all your collections. It works with the single-choice elements (`filter-list`, `filter-select`, `filter-dropdown`, `filter-popover`, `filter-swatches`), not with `filter-checkbox`. While a collection is chosen, the text search looks inside it: see [Search inside a collection](#search-inside-a-collection).

### Text search

`filter-search` holds a Form Block with a Text field. Typing searches after a short pause. If the form has a submit button, the search runs on submit instead. It reads no `label`.

### Search inside a collection

The text search also works on a collection: with `data-collection`, or while the visitor has chosen a collection in a Collection filter. Shopify has no search inside a collection, so Smootify compares two lists and shows the products in both:

- the results of the search, up to 1,000, Shopify’s limit;
- the products of the collection, up to 2,000. In a bigger collection, only the first 2,000 are compared.

What changes compared with a plain search:

- **Order.** Relevance and price follow the order of the search. Best selling, title and date follow the order of the collection.
- **Filters.** Their values and counts come from the search, so they count the whole catalog, not only the collection.
- **Pages.** Previous and next, and the page numbers, work as usual.

**For an exact search, use a tag.** On a large or important collection, give its products a tag in Shopify, such as `summer`, and add `data-tag="summer"` to the block next to `data-collection`:

- without a search, the block still shows the collection, in its order;
- with a search, Smootify searches only the products with that tag. The results are exact, and the filters count only those products.

`data-tag` also works without `data-collection`: the block then always lists the products with that tag.

> [!WARNING]
> `data-tag` needs the **Tag** filter turned on in Shopify's Search & Discovery app. Without it, Shopify ignores the tag without an error, and the tag narrows nothing. The debugger tells you when this happens.

## 3. Sorting

A sort element goes inside `smootify-search-discovery`, like a filter, with a Form Block inside it. There are four:

| Element | Built with | Good to know |
|---|---|---|
| `sort-select` | a Form Block with a Select field | the first option stays as the placeholder |
| `sort-radio` | a Form Block with one Radio button | the Radio button is the template of every option |
| `sort-dropdown` | a Form Block and a Webflow Dropdown | the toggle shows the current sort |
| `sort-popover` | a Form Block and links or buttons in a popover | the trigger shows the current sort |

The options come from Shopify, and depend on the list:

- the whole catalog: relevance, and price low to high and high to low;
- one collection (`data-collection`, or a chosen Collection filter): also best selling, title A to Z and Z to A, and oldest and newest first.

`selected-value` on the sort element picks an option when the page opens, by its label, such as `selected-value="Price (Low to High)"`. Without it the store’s own order stays, and the trigger keeps the text you wrote in Webflow, such as “Featured”, until the visitor picks an option: Shopify does not tell which order a collection uses. Next to the element that shows the chosen order, a label of yours such as “Sort:” stays as you wrote it: the trigger then reads “Sort: Price (Low to High)”.

### Sort labels

Each option has an English label, and each list shows only some of them:

| Key | Label | Shown for |
|---|---|---|
| `relevance` | Relevance | every list |
| `price_asc` | Price (Low to High) | every list |
| `price_desc` | Price (High to Low) | every list |
| `best_selling` | Best Selling | one collection |
| `title_asc` | Title (A - Z) | one collection |
| `title_dsc` | Title (Z - A) | one collection |
| `created_asc` | Oldest to newest | one collection |
| `created_dsc` | Newest to oldest | one collection |

To change them, go to **Site settings › Custom code** and add this script to **Head code**. Write only the labels you change: the others keep their English text.

```html
<script>
  window.SmootifySortLabels = {
    relevance: "Featured",
    price_asc: "Price, low to high",
    price_desc: "Price, high to low",
    best_selling: "Best sellers",
    title_asc: "Name, A to Z",
    title_dsc: "Name, Z to A",
    created_asc: "Oldest first",
    created_dsc: "Newest first",
  };
</script>
```

`selected-value` matches the label as shown: after a change, write the new label, such as `selected-value="Price, low to high"`.

**Several languages.** With Webflow Localization, each locale sets the page language in `<html lang>`. Pick the labels from it, and keep the English ones for the other locales:

```html
<script>
  window.SmootifySortLabels = {
    it: { relevance: "In evidenza", price_asc: "Prezzo crescente", price_desc: "Prezzo decrescente" },
    fr: { relevance: "Pertinence", price_asc: "Prix croissant", price_desc: "Prix décroissant" },
  }[document.documentElement.lang.slice(0, 2)] || {};
</script>
```

## 4. Count, active filters and reset

These elements go anywhere inside `smootify-search-discovery`. All are optional.

- **`filter="count"`** on text shows the total number of results.
- **`filter="active"`** is the template of one active filter: a Div block, a link or a Custom element with the tag `button`. Smootify makes one copy per active filter, and a click on a copy removes that filter. Inside it, **`filter="active-label"`** shows “Colour: Red”.
- **`filter="active-count"`** shows how many filters the visitor can remove.
- **A reset button**: a Custom element with the tag `button` and `type="reset"`. It removes every filter the visitor can remove, and the search words. It is disabled and gets `is-disabled` when there is nothing to remove.
- **`filter="empty-state"`** on a Div block shows only when no product matches.

```html
<div>
  <div filter="active"><div filter="active-label">Colour: Red</div></div>
  <button type="reset">Clear all</button>
</div>
<div filter="empty-state">No products match these filters.</div>
```

Filters the visitor cannot remove, such as a value preselected without `allow-reset`, come first among the active filters, with the class `preselected-filter`, and a click on them does nothing.

## 5. Pagination

Pagination buttons are Custom elements with the tag `button` inside `smootify-search-discovery`. Their `data-action` decides how the visitor moves through the results:

| Way | `data-action` | Shows | Good to know |
|---|---|---|---|
| Previous and next | `previous`, `next` | one page at a time | the best choice for large catalogs |
| Page numbers | `page` | one page at a time, with the numbers to jump to, between previous and next | on a collection, it needs the Availability filter (see Page numbers) |
| Load more | `load` | the next page, added below the products shown, on a click | every page adds cards, which slows phones down on large catalogs |
| Load as the visitor scrolls | `load-in-view` | the next page, added below when the button scrolls into view | every page adds cards, which slows phones down on large catalogs |

A button is hidden and disabled when there is no such page. Use `previous` and `next`, or one of the two load buttons, not all of them. Buttons inside a product card, such as the previous and next of a booking calendar, are not pagination buttons.

When new results appear, the page scrolls to the block. `disable-scroll` on `smootify-search-discovery` turns this off, and `disable-scroll-on-load-more` turns it off for the load buttons only.

### Page numbers

Next to previous and next, the block can show page numbers, such as `1 … 4 5 6 … 12`:

```html
<smootify-search-discovery limit="24" page-range="1">
  <div>
    <smootify-product data-id="filter"><a product="url" href="#">Product name</a></smootify-product>
  </div>
  <div>
    <button data-action="previous">Previous</button>
    <button data-action="page"><span filter="page-number">1</span></button>
    <div filter="page-ellipsis">…</div>
    <button data-action="next">Next</button>
  </div>
  <div>Page <span filter="page-current">1</span> of <span filter="page-total">12</span></div>
</smootify-search-discovery>
```

- **One number button.** A Custom element with the tag `button` and `data-action="page"` is the template of every number: Smootify copies it once per number shown. Inside it, `filter="page-number"` holds the number; without it, the number becomes the button’s text. The current page gets the class `is-active` and cannot be clicked: style it with a combo class.
- **The gap.** `filter="page-ellipsis"` is the template of the gaps between numbers, such as `…`. It is optional. When the gap is a single page, that page’s number shows instead.
- **How many numbers.** `page-range` on `smootify-search-discovery` sets how many pages show on each side of the current one, `1` by default. The first and the last page always show.
- **“Page 3 of 12”.** `filter="page-current"` and `filter="page-total"` on text. The total stays empty until Smootify knows it.
- **The class `has-page-numbers`** is on `smootify-search-discovery` while there is more than one page to number: use it to hide the number bar otherwise.
- **In the address.** With `sync-query-params`, the address also gets the page from the second one on, such as `?page=3`, and a link with `?page=3` opens that page.

> [!NOTE]
> On a **collection**, page numbers need the **Availability** filter turned on in Shopify's Search & Discovery app. Shopify does not give the number of products in a collection, so Smootify adds it up from that filter. Without it the numbers do not show (previous and next still work), `filter="count"` shows only the products of the page, and on `.webflow.io` the debugger says why. A **search** counts at most 1,000 results, a Shopify limit.

Opening a page far from the current one takes a moment longer: Shopify gives the pages one after the other, so Smootify first reads the pages before it, with small requests. The pages already seen, and the next and previous ones, open at once.

## 6. Links, mobile panels and an apply button

**Filters in the address.** With `sync-query-params` on `smootify-search-discovery`, the address follows the filters, such as `?Color=Red`, so a filtered page can be shared. Locked filters stay out of it. Even without it, a link to the page with a filter’s label and a value, such as `/shop?Color=Red`, opens it with that filter on, as long as the page has a filter element with that label. `?q=` followed by words opens it with that search.

**Filters outside the block.** Filters and sort elements can sit in a panel elsewhere on the page, such as a side panel on mobile. Give the panel an ID in its element settings, and add `data-expand="#filters"` (with that ID) to `smootify-search-discovery`.

**Filters in a popover.** With `auto-close-details="true"` on `smootify-search-discovery`, a popover of filters inside it closes once the results change. See [Popovers](https://www.smootify.io/docs/build/popovers).

**An apply button.** A Custom element with the tag `button` and `type="submit"`, inside the block but outside the filters’ forms, changes how filters apply: a choice updates the filters only, and the products reload when the visitor clicks the button.

## Other settings

On `smootify-search-discovery`:

- `animation` (`fade` by default, `slide`, `scale`, `fadeBlurred`, `slideBlurred` or `none`), `duration`, `easing` and `stagger` set how new cards appear;
- `unavailable-products` sets where products that cannot be bought go: `LAST` (the default), `HIDE` or `SHOW`. It does not apply inside a collection;
- `avoid-webflow-restart` skips restarting Webflow interactions after each render.

Every attribute is in [`smootify-search-discovery`](https://www.smootify.io/docs/reference/elements/smootify-search-discovery) and [search & discovery attributes](https://www.smootify.io/docs/reference/attributes/search-discovery).

**While it loads.** Add `skeleton` to the filters, the count and the card elements that should shimmer until the first results. Smootify removes it when it writes them. See [loading](https://www.smootify.io/docs/reference/attributes/loading).

## Before you publish

Shopify’s own limits apply: a collection with more than 5,000 products, or a search with more than 100,000 results, shows no filters, and a filter shows at most 100 values. When a filter has more, group similar values in the app: a group holds at most 200 values, and a store can have at most 1,000 groups.

> [!STUCK]
> - **A filter never shows.** Its `label` is not written exactly as in Shopify's Search & Discovery app, or none of the listed products has a value for it.
> - **A filter element disappears.** It has no Form Block inside, or it sits outside `smootify-search-discovery` and outside the `data-expand` panel.
> - **Every card shows the same name or image.** Those elements are bound to CMS fields only. Add `product="title"`, `variant="image"` and the other attributes.
> - **Only three sort options.** Best selling, title and date exist only inside a collection.
> - **`data-tag` changes nothing.** The **Tag** filter is off in Shopify's Search & Discovery app. Turn it on.
> - **`data-vendor` changes nothing.** The **Vendor** filter is off in Shopify's Search & Discovery app. Turn it on.
> - **A search in a big collection misses products.** Only the first 2,000 products of the collection are compared with the search. Give the collection's products a tag and use `data-tag`.
> - **The count shows only the products of the page, and no page numbers appear.** On a collection, the total comes from the **Availability** filter. Turn it on in Shopify's Search & Discovery app.
> - **The page is slow on phones.** Lower `limit`, and use `previous` and `next` instead of loading more.

## Coming from 1.x

- Nothing shimmers by default any more: add `skeleton` where you want it.
- `filter="count"` is now the total number of results; 1.x showed the products of the page.
- `filter-search` reads no `label`, and `filter-price` is always the price filter.
- `filter-swatches` keeps its first button and removes the others, a submit button included.
- The text search works on collection pages: 1.x hid `filter-search` there with the `hide-search` class, which 2.0 no longer sets. Remove a shared `filter-search` from the collection pages if they should not have one.
