Skip to content

Commerce Foundations is our pre-built Craft Commerce store. See what it includes

Recipe: filter variants based on a selection

Storefront examples that filter a product’s variants by attribute, showing the matching variants in a list. Two variations: filter by all selected values (AND), or by any of the selected values (OR). Each shown using Sprig and Alpine.js.

Replace variantAttributes with the handle of your Variant Attributes field.

Filter by all selected values

Returns only variants that match every attribute the user has chosen.

Sprig

{% set product = craft.products.id(productId).one() %}
{% set selection = {} %}

<h1>{{ product.title }}</h1>
<div>
  {% for attribute in craft.variantManager.getAttributeOptions(productId) %}
    {% set selected = _context['attributeValue_' ~ loop.index] ?? (selection[attribute.name] ?? null) %}

    {% if selected is not null and selected != '' %}
      {% set selection = selection|merge({(attribute.name): selected}) %}
    {% endif %}
    <div>
      <label>
        {{ attribute.name }} - {{ selected }}
        <select sprig name="attributeValue_{{ loop.index }}">
          <option value="" {{ selected is null or selected == '' ? 'selected' }}>Select an option</option>
          {% for value in attribute.values %}
            <option value="{{ value }}" {{ selected == value ? 'selected' }}>{{ value }}</option>
          {% endfor %}
        </select>
      </label>
    </div>
  {% endfor %}
</div>

<ul>
  {% for variant in craft.variants().productId(productId).variantAttributes(selection).all() %}
    <li>{{ variant.sku }} - {{ variant.price|commerceCurrency }}</li>
  {% endfor %}
</ul>

Alpine.js

{% set product = craft.products.id(productId).one() %}
{% set selection = {} %}

<h1>{{ product.title }}</h1>
<div>
  {% for attribute in craft.variantManager.getAttributeOptions(productId) %}
    {% set selected = craft.app.request.getParam('attributeValue_' ~ loop.index) ?? (selection[attribute.name] ?? null) %}

    {% if selected is not null and selected != '' %}
      {% set selection = selection|merge({(attribute.name): selected}) %}
    {% endif %}
    <div>
      <label>
        {{ attribute.name }} - {{ selected }}
        <select name="attributeValue_{{ loop.index }}" @change="attributeChanged">
          <option value="" {{ selected is null or selected == '' ? 'selected' }}>Select an option</option>
          {% for value in attribute.values %}
            <option value="{{ value }}" {{ selected == value ? 'selected' }}>{{ value }}</option>
          {% endfor %}
        </select>
      </label>
    </div>
  {% endfor %}
</div>

<ul>
  {% for variant in craft.variants().productId(productId).variantAttributes(selection).all() %}
    <li>{{ variant.sku }} - {{ variant.price|commerceCurrency }}</li>
  {% endfor %}
</ul>

{% js %}
  function attributeChanged(event) {
    const queryParams = new URLSearchParams(window.location.search);
    queryParams.set(event.target.name, event.target.value);
    window.location.search = queryParams.toString();
  }
{% endjs %}

Filter by any selected value

The selection is built as a list of single-pair objects, which the field treats as an OR filter.

Sprig

{% set product = craft.products.id(productId).one() %}
{% set selection = [] %}

<h1>{{ product.title }}</h1>
<div>
  {% for attribute in craft.variantManager.getAttributeOptions(productId) %}
    {% set selected = _context['attributeValue_' ~ loop.index] ?? null %}

    {% if selected is not null and selected != '' %}
      {% set selection = selection|merge([{(attribute.name): selected}]) %}
    {% endif %}
    <div>
      <label>
        {{ attribute.name }} - {{ selected }}
        <select sprig name="attributeValue_{{ loop.index }}">
          <option value="" {{ selected is null or selected == '' ? 'selected' }}>Select an option</option>
          {% for value in attribute.values %}
            <option value="{{ value }}" {{ selected == value ? 'selected' }}>{{ value }}</option>
          {% endfor %}
        </select>
      </label>
    </div>
  {% endfor %}
</div>

<ul>
  {% for variant in craft.variants().productId(productId).variantAttributes(selection).all() %}
    <li>{{ variant.sku }} - {{ variant.price|commerceCurrency }}</li>
  {% endfor %}
</ul>

Alpine.js

{% set product = craft.products.id(productId).one() %}
{% set selection = [] %}

<h1>{{ product.title }}</h1>
<div>
  {% for attribute in craft.variantManager.getAttributeOptions(productId) %}
    {% set selected = craft.app.request.getParam('attributeValue_' ~ loop.index) ?? null %}

    {% if selected is not null and selected != '' %}
      {% set selection = selection|merge([{(attribute.name): selected}]) %}
    {% endif %}
    <div>
      <label>
        {{ attribute.name }} - {{ selected }}
        <select name="attributeValue_{{ loop.index }}" @change="attributeChanged">
          <option value="" {{ selected is null or selected == '' ? 'selected' }}>Select an option</option>
          {% for value in attribute.values %}
            <option value="{{ value }}" {{ selected == value ? 'selected' }}>{{ value }}</option>
          {% endfor %}
        </select>
      </label>
    </div>
  {% endfor %}
</div>

<ul>
  {% for variant in craft.variants().productId(productId).variantAttributes(selection).all() %}
    <li>{{ variant.sku }} - {{ variant.price|commerceCurrency }}</li>
  {% endfor %}
</ul>

{% js %}
  function attributeChanged(event) {
    const queryParams = new URLSearchParams(window.location.search);
    queryParams.set(event.target.name, event.target.value);
    window.location.search = queryParams.toString();
  }
{% endjs %}

Think we might be a fit?

Start a conversation
See if we’re a fit