Products & Variants

Your product catalog in Commerce is represented by a pair of element types.

Every product must have one or more variants, and every variant belongs to a single product.

To illustrate the relationship between products and variants, consider the needs of a store that sells apparel.

A particular pair of tennis shoes is available in two colors, and in U.S. half-size increments from 6 to 12. The product we’re describing might have a name like “Court Balance DX,” while the variants represent each intersection of a color and size. This is both a key discovery tool (customers need to find shoes that fit their feet and style), and a necessary business tool for the store owner (tracking inventory, accounting for shipping weight, visualizing sales).

In this example, the product would hold all the marketing information like text, photos, and graphics, while the variants represent unique, saleable variations. Customers shop for a shoe that is aligned with their needs and tastes, but buy a specific size and color.

# Products

Products organize your goods into logical bundles of variants. A product itself is never actually purchased—what goes into a cart for purchase is one of the product’s variants. In this way, the product is free to house some globally-relevant attributes or content, while the variants describe specific physical or digital items.

In the same way that Craft’s native element types each share a set of common attributes, every Commerce product has a Title, Slug, Post Date, Expiry Date, and per-site status options.

# Product Types

Product Types provide a way to distinguish classes of goods in your stores. Manage product types in the control panel from

  1. Commerce
  2. System Settings
  3. Product Types
.

Despite their similarities to entries, a product’s type cannot be changed after it is created.

# Product Type Options

Each product type has the following settings.

Name

The name of the product type as displayed in the control panel. Customers only see this if you explicitly output it in the front-end (or in an email or PDF).

Handle

The handle is what you’ll use to reference the product type in code. In Twig, you would query for products with the clothes type like this:

{% set clothes = craft.products()
  .type('clothes')
  .all() %}
Versioning

Enable versioning to queue and revert product content changes with Craft’s drafts and revisions system.

Titles

When enabled, each product will require a Title. Disable this if you’d like to generate titles with an object template, using values of other attributes or fields.

Automatic SKU Format

Defines what auto-generated SKUs should look like when a variant’s SKU field is left empty. This setting is an object template, meaning it can include dynamic values such as {product.slug} or {myCustomField}.

The SKU format is always evaluated in the context of a variant, so product attributes must be prefixed with product, like {product.myCustomField}.

Commerce requires that SKUs are unique across all variants in the system—including anything in the trash, so avoid using static or ambiguous values (like PLACEHOLDER) that are apt to collide when first saving a variant.

Order Description Format

Identifies a variant in the cart. Like the SKU format, this is also an object template, and gets rendered in the context of a variant. It can include tags that output properties, such as {product.title} or {myVariantCustomField}.

The rendered string is ultimately stored in a line item’s description attribute. Changing a description format after an order has been completed does not apply retroactively.

Max Variants

To limit products of this type to a single variant, use 1 in this field, or leave it blank for no limit.

Show the Dimensions and Weight fields

Allows you to hide the weight and dimensions fields if they are not necessary for products of this type.

Show the Title field for variants

Whether or not to show the “Variant Title” field when adding or editing variants. When true a “Variant Title Field Label” will appear, allowing you to change what the “Variant Title” field label should be.

Site Settings

Like entries, products provide site-specific routing settings. When a customer visits a product’s URL in a given site, Commerce renders the specified template with a special product variable.

# Tax & Shipping

The second tab in a product type’s settings screen is strictly informational—it displays a list of Shipping Categories and Tax Categories (from the Store Management area) that can be selected from variants of current product type.

Product types are defined globally, but shipping and tax categories are defined per-store. If you have similarly-named categories in multiple stores, you may see them listed twice.

# Product Fields

Every product type’s authoring experience can be tailored to its needs through a field layout. Consider what content belongs on a product and what belongs on a variant.

A product’s field layout must include the special Variants field layout element, which controls where the nested element management interface lives.

# Variant Fields

In addition to fields associated with a product, the product type defines what fields are available to its nested variants.

# Templating

There are a ton of ways to leverage your product types and product data in templates. Keep in mind that the custom fields available to each product type may differ—but they’re accessed exactly the same way as you would with any other element type!

# Displaying a Product Type

Every product has access to its product type definition via a type attribute:

<ul class="breadcrumbs">
  <li><a href="{{ siteUrl }}">Home</a></li>
  <li><a href="{{ siteUrl('shop') }}">Shop</a></li>
  <li><a href="{{ siteUrl("shop/#{product.type.handle}") }}">{{ product.type.name }}</a></li>
</ul>

<h2>{{ product.title }}</h2>

This example generates a URL to an “index” for a product type—but requires that we set up a corresponding route that maps it to a template:

return [
    // ...
    'shop/<productType:{slug}>' => ['template' => '_shop/product-type'],
];

# Listing Product Types

Returns an array of all product types set up in the system.

{% for type in craft.commerce.productTypes.allProductTypes %}
  {{ type.handle }} - {{ type.name }}
{% endfor %}

# Querying Products

You can fetch products using product queries.

{# Create a new product query #}
{% set myProductQuery = craft.products() %}

Once you’ve created a product query, you can set parameters on it to narrow down the results, and then execute it by calling .all(). An array of Product elements will be returned.

See Element Queries in the Craft docs to learn about how element queries work.

# Example

We can use Twig to display the ten most recently-added Clothing products:

  1. Create a product query with craft.products().
  2. Set the type and limit parameters on it.
  3. Fetch the products with .all().
  4. Loop through the products using a for (opens new window) tag to output their HTML.
{# Create a product query with the 'type' and 'limit' parameters #}
{% set newProducts = craft.products()
  .type('clothing')
  .limit(10)
  .all() %}

{# Display the products #}
{% for product in newProducts %}
  <h2><a href="{{ product.url }}">{{ product.title }}</a></h2>
  {{ product.summary|md }}
  <a href="{{ product.url }}">Learn more</a>
{% endfor %}

To fetch the same information with GraphQL, we could write a query like this:

{
  products(limit: 10, type: "clothing") {
    title
    uri
    ... on clothing_Product {
      summary
    }
  }
}

# Product Query Parameters

Product queries support the following parameters:

Param Description
addOrderBy Adds additional ORDER BY columns to the query.
after Narrows the query results to only products that were posted on or after a certain date.
afterPopulate Performs any post-population processing on elements.
andRelatedTo Narrows the query results to only products that are related to certain other elements.
asArray Causes the query to return matching products as arrays of data, rather than Product objects.
before Narrows the query results to only products that were posted before a certain date.
cache Enables query cache for this Query.
clearCachedResult Clears the cached result (opens new window).
dateCreated Narrows the query results based on the products’ creation dates.
dateUpdated Narrows the query results based on the products’ last-updated dates.
defaultHeight Narrows the query results based on the products’ default variant height dimension IDs.
defaultLength Narrows the query results based on the products’ default variant length dimension IDs.
defaultPrice Narrows the query results based on the products’ default variant price.
defaultSku Narrows the query results based on the default productvariants defaultSku
defaultWeight Narrows the query results based on the products’ default variant weight dimension IDs.
defaultWidth Narrows the query results based on the products’ default variant width dimension IDs.
eagerly Causes the query to be used to eager-load results for the query’s source element and any other elements in its collection.
expiryDate Narrows the query results based on the products’ expiry dates.
fields Returns the list of fields that should be returned by default by toArray() (opens new window) when no specific fields are specified.
fixedOrder Causes the query results to be returned in the order specified by id.
hasVariant Narrows the query results to only products that have certain variants.
id Narrows the query results based on the products’ IDs.
ignorePlaceholders Causes the query to return matching products as they are stored in the database, ignoring matching placeholder elements that were set by craft\services\Elements::setPlaceholderElement() (opens new window).
inBulkOp Narrows the query results to only products that were involved in a bulk element operation.
inReverse Causes the query results to be returned in reverse order.
language Determines which site(s) the products should be queried in, based on their language.
limit Determines the number of products that should be returned.
offset Determines how many products should be skipped in the results.
orderBy Determines the order that the products should be returned in. (If empty, defaults to postDate DESC.)
postDate Narrows the query results based on the products’ post dates.
preferSites If unique is set, this determines which site should be selected when querying multi-site elements.
prepForEagerLoading Prepares the query for lazy eager loading.
prepareSubquery Prepares the element query and returns its subquery (which determines what elements will be returned).
relatedTo Narrows the query results to only products that are related to certain other elements.
render Executes the query and renders the resulting elements using their partial templates.
search Narrows the query results to only products that match a search query.
site Determines which site(s) the products should be queried in.
siteId Determines which site(s) the products should be queried in, per the site’s ID.
siteSettingsId Narrows the query results based on the products’ IDs in the elements_sites table.
slug Narrows the query results based on the products’ slugs.
status Narrows the query results based on the products’ statuses.
title Narrows the query results based on the products’ titles.
trashed Narrows the query results to only products that have been soft-deleted.
type Narrows the query results based on the products’ types.
typeId Narrows the query results based on the products’ types, per the types’ IDs.
uid Narrows the query results based on the products’ UIDs.
unique Determines whether only elements with unique IDs should be returned by the query.
uri Narrows the query results based on the products’ URIs.
wasCountEagerLoaded Returns whether the query result count was already eager loaded by the query's source element.
wasEagerLoaded Returns whether the query results were already eager loaded by the query's source element.
with Causes the query to return matching products eager-loaded with related elements.
withCustomFields Sets whether custom fields should be factored into the query.

# addOrderBy

Adds additional ORDER BY columns to the query.

# after

Narrows the query results to only products that were posted on or after a certain date.

Possible values include:

Value Fetches products…
'2018-04-01' that were posted after 2018-04-01.
a DateTime (opens new window) object that were posted after the date represented by the object.
{# Fetch products posted this month #}
{% set firstDayOfMonth = date('first day of this month') %}

{% set products = craft.products()
  .after(firstDayOfMonth)
  .all() %}

# afterPopulate

Performs any post-population processing on elements.

# andRelatedTo

Narrows the query results to only products that are related to certain other elements.

See Relations (opens new window) for a full explanation of how to work with this parameter.

{# Fetch all products that are related to myCategoryA and myCategoryB #}
{% set products = craft.products()
  .relatedTo(myCategoryA)
  .andRelatedTo(myCategoryB)
  .all() %}

# asArray

Causes the query to return matching products as arrays of data, rather than Product objects.

{# Fetch products as arrays #}
{% set products = craft.products()
  .asArray()
  .all() %}

# before

Narrows the query results to only products that were posted before a certain date.

Possible values include:

Value Fetches products…
'2018-04-01' that were posted before 2018-04-01.
a DateTime (opens new window) object that were posted before the date represented by the object.
{# Fetch products posted before this month #}
{% set firstDayOfMonth = date('first day of this month') %}

{% set products = craft.products()
  .before(firstDayOfMonth)
  .all() %}

# cache

Enables query cache for this Query.

# clearCachedResult

Clears the cached result (opens new window).

# dateCreated

Narrows the query results based on the products’ creation dates.

Possible values include:

Value Fetches products…
'>= 2018-04-01' that were created on or after 2018-04-01.
'< 2018-05-01' that were created before 2018-05-01.
['and', '>= 2018-04-04', '< 2018-05-01'] that were created between 2018-04-01 and 2018-05-01.
now/today/tomorrow/yesterday that were created at midnight of the specified relative date.
{# Fetch products created last month #}
{% set start = date('first day of last month')|atom %}
{% set end = date('first day of this month')|atom %}

{% set products = craft.products()
  .dateCreated(['and', ">= #{start}", "< #{end}"])
  .all() %}

# dateUpdated

Narrows the query results based on the products’ last-updated dates.

Possible values include:

Value Fetches products…
'>= 2018-04-01' that were updated on or after 2018-04-01.
'< 2018-05-01' that were updated before 2018-05-01.
['and', '>= 2018-04-04', '< 2018-05-01'] that were updated between 2018-04-01 and 2018-05-01.
now/today/tomorrow/yesterday that were updated at midnight of the specified relative date.
{# Fetch products updated in the last week #}
{% set lastWeek = date('1 week ago')|atom %}

{% set products = craft.products()
  .dateUpdated(">= #{lastWeek}")
  .all() %}

# defaultHeight

Narrows the query results based on the products’ default variant height dimension IDs.

Possible values include:

Value Fetches products…
1 of a type with a dimension of 1.
'not 1' not a dimension of 1.
[1, 2] of a a dimension 1 or 2.
['and', '>= ' ~ 100, '<= ' ~ 2000] of a dimension between 100 and 2000
{# Fetch products of the product default dimension of 1 #}
{% set products = craft.products()
  .defaultHeight(1)
  .all() %}

# defaultLength

Narrows the query results based on the products’ default variant length dimension IDs.

Possible values include:

Value Fetches products…
1 of a type with a dimension of 1.
'not 1' not a dimension of 1.
[1, 2] of a a dimension 1 or 2.
['and', '>= ' ~ 100, '<= ' ~ 2000] of a dimension between 100 and 2000
{# Fetch products of the product default dimension of 1 #}
{% set products = craft.products()
  .defaultLength(1)
  .all() %}

# defaultPrice

Narrows the query results based on the products’ default variant price.

Possible values include:

Value Fetches products…
10 of a price of 10.
['and', '>= ' ~ 100, '<= ' ~ 2000] of a default variant price between 100 and 2000
{# Fetch products of the product type with an ID of 1 #}
{% set products = craft.products()
  .defaultPrice(1)
  .all() %}

# defaultSku

Narrows the query results based on the default productvariants defaultSku

Possible values include:

Value Fetches products…
xxx-001 of products default SKU of xxx-001.
'not xxx-001' not a default SKU of xxx-001.
['not xxx-001', 'not xxx-002'] of a default SKU of xxx-001 or xxx-002.
['not',xxx-001,xxx-002] not a product default SKU of xxx-001 or xxx-001.
{# Fetch products of the product default SKU of `xxx-001` #}
{% set products = craft.products()
  .defaultSku('xxx-001')
  .all() %}

# defaultWeight

Narrows the query results based on the products’ default variant weight dimension IDs.

Possible values include:

Value Fetches products…
1 of a type with a dimension of 1.
'not 1' not a dimension of 1.
[1, 2] of a a dimension 1 or 2.
['and', '>= ' ~ 100, '<= ' ~ 2000] of a dimension between 100 and 2000
{# Fetch products of the product default dimension of 1 #}
{% set products = craft.products()
  .defaultWeight(1)
  .all() %}

# defaultWidth

Narrows the query results based on the products’ default variant width dimension IDs.

Possible values include:

Value Fetches products…
1 of a type with a dimension of 1.
'not 1' not a dimension of 1.
[1, 2] of a a dimension 1 or 2.
['and', '>= ' ~ 100, '<= ' ~ 2000] of a dimension between 100 and 2000
{# Fetch products of the product default dimension of 1 #}
{% set products = craft.products()
  .defaultWidth(1)
  .all() %}

# eagerly

Causes the query to be used to eager-load results for the query’s source element and any other elements in its collection.

# expiryDate

Narrows the query results based on the products’ expiry dates.

Possible values include:

Value Fetches products…
'>= 2020-04-01' that will expire on or after 2020-04-01.
'< 2020-05-01' that will expire before 2020-05-01
['and', '>= 2020-04-04', '< 2020-05-01'] that will expire between 2020-04-01 and 2020-05-01.
{# Fetch products expiring this month #}
{% set nextMonth = date('first day of next month')|atom %}

{% set products = craft.products()
  .expiryDate("< #{nextMonth}")
  .all() %}

# fields

Returns the list of fields that should be returned by default by toArray() (opens new window) when no specific fields are specified.

A field is a named element in the returned array by toArray() (opens new window). This method should return an array of field names or field definitions. If the former, the field name will be treated as an object property name whose value will be used as the field value. If the latter, the array key should be the field name while the array value should be the corresponding field definition which can be either an object property name or a PHP callable returning the corresponding field value. The signature of the callable should be:

function ($model, $field) {
    // return field value
}

For example, the following code declares four fields:

  • email: the field name is the same as the property name email;
  • firstName and lastName: the field names are firstName and lastName, and their values are obtained from the first_name and last_name properties;
  • fullName: the field name is fullName. Its value is obtained by concatenating first_name and last_name.
return [
    'email',
    'firstName' => 'first_name',
    'lastName' => 'last_name',
    'fullName' => function ($model) {
        return $model->first_name . ' ' . $model->last_name;
    },
];

# fixedOrder

Causes the query results to be returned in the order specified by id.

If no IDs were passed to id, setting this to true will result in an empty result set.

{# Fetch products in a specific order #}
{% set products = craft.products()
  .id([1, 2, 3, 4, 5])
  .fixedOrder()
  .all() %}

# hasVariant

Narrows the query results to only products that have certain variants.

Possible values include:

Value Fetches products…
a VariantQuery object with variants that match the query.

# id

Narrows the query results based on the products’ IDs.

Possible values include:

Value Fetches products…
1 with an ID of 1.
'not 1' not with an ID of 1.
[1, 2] with an ID of 1 or 2.
['not', 1, 2] not with an ID of 1 or 2.
{# Fetch the product by its ID #}
{% set product = craft.products()
  .id(1)
  .one() %}

This can be combined with fixedOrder if you want the results to be returned in a specific order.

# ignorePlaceholders

Causes the query to return matching products as they are stored in the database, ignoring matching placeholder elements that were set by craft\services\Elements::setPlaceholderElement() (opens new window).

# inBulkOp

Narrows the query results to only products that were involved in a bulk element operation.

# inReverse

Causes the query results to be returned in reverse order.

{# Fetch products in reverse #}
{% set products = craft.products()
  .inReverse()
  .all() %}

# language

Determines which site(s) the products should be queried in, based on their language.

Possible values include:

Value Fetches products…
'en' from sites with a language of en.
['en-GB', 'en-US'] from sites with a language of en-GB or en-US.
['not', 'en-GB', 'en-US'] not in sites with a language of en-GB or en-US.

Elements that belong to multiple sites will be returned multiple times by default. If you only want unique elements to be returned, use unique in conjunction with this.

{# Fetch products from English sites #}
{% set products = craft.products()
  .language('en')
  .all() %}

# limit

Determines the number of products that should be returned.

{# Fetch up to 10 products  #}
{% set products = craft.products()
  .limit(10)
  .all() %}

# offset

Determines how many products should be skipped in the results.

{# Fetch all products except for the first 3 #}
{% set products = craft.products()
  .offset(3)
  .all() %}

# orderBy

Determines the order that the products should be returned in. (If empty, defaults to postDate DESC.)

{# Fetch all products in order of date created #}
{% set products = craft.products()
  .orderBy('dateCreated ASC')
  .all() %}

# postDate

Narrows the query results based on the products’ post dates.

Possible values include:

Value Fetches products…
'>= 2018-04-01' that were posted on or after 2018-04-01.
'< 2018-05-01' that were posted before 2018-05-01
['and', '>= 2018-04-04', '< 2018-05-01'] that were posted between 2018-04-01 and 2018-05-01.
{# Fetch products posted last month #}
{% set start = date('first day of last month')|atom %}
{% set end = date('first day of this month')|atom %}

{% set products = craft.products()
  .postDate(['and', ">= #{start}", "< #{end}"])
  .all() %}

# preferSites

If unique is set, this determines which site should be selected when querying multi-site elements.

For example, if element “Foo” exists in Site A and Site B, and element “Bar” exists in Site B and Site C, and this is set to ['c', 'b', 'a'], then Foo will be returned for Site B, and Bar will be returned for Site C.

If this isn’t set, then preference goes to the current site.

{# Fetch unique products from Site A, or Site B if they don’t exist in Site A #}
{% set products = craft.products()
  .site('*')
  .unique()
  .preferSites(['a', 'b'])
  .all() %}

# prepForEagerLoading

Prepares the query for lazy eager loading.

# prepareSubquery

Prepares the element query and returns its subquery (which determines what elements will be returned).

# relatedTo

Narrows the query results to only products that are related to certain other elements.

See Relations (opens new window) for a full explanation of how to work with this parameter.

{# Fetch all products that are related to myCategory #}
{% set products = craft.products()
  .relatedTo(myCategory)
  .all() %}

# render

Executes the query and renders the resulting elements using their partial templates.

If no partial template exists for an element, its string representation will be output instead.

Narrows the query results to only products that match a search query.

See Searching (opens new window) for a full explanation of how to work with this parameter.

{# Get the search query from the 'q' query string param #}
{% set searchQuery = craft.app.request.getQueryParam('q') %}

{# Fetch all products that match the search query #}
{% set products = craft.products()
  .search(searchQuery)
  .all() %}

# site

Determines which site(s) the products should be queried in.

The current site will be used by default.

Possible values include:

Value Fetches products…
'foo' from the site with a handle of foo.
['foo', 'bar'] from a site with a handle of foo or bar.
['not', 'foo', 'bar'] not in a site with a handle of foo or bar.
a craft\models\Site (opens new window) object from the site represented by the object.
'*' from any site.

If multiple sites are specified, elements that belong to multiple sites will be returned multiple times. If you only want unique elements to be returned, use unique in conjunction with this.

{# Fetch products from the Foo site #}
{% set products = craft.products()
  .site('foo')
  .all() %}

# siteId

Determines which site(s) the products should be queried in, per the site’s ID.

The current site will be used by default.

Possible values include:

Value Fetches products…
1 from the site with an ID of 1.
[1, 2] from a site with an ID of 1 or 2.
['not', 1, 2] not in a site with an ID of 1 or 2.
'*' from any site.
{# Fetch products from the site with an ID of 1 #}
{% set products = craft.products()
  .siteId(1)
  .all() %}

# siteSettingsId

Narrows the query results based on the products’ IDs in the elements_sites table.

Possible values include:

Value Fetches products…
1 with an elements_sites ID of 1.
'not 1' not with an elements_sites ID of 1.
[1, 2] with an elements_sites ID of 1 or 2.
['not', 1, 2] not with an elements_sites ID of 1 or 2.
{# Fetch the product by its ID in the elements_sites table #}
{% set product = craft.products()
  .siteSettingsId(1)
  .one() %}

# slug

Narrows the query results based on the products’ slugs.

Possible values include:

Value Fetches products…
'foo' with a slug of foo.
'foo*' with a slug that begins with foo.
'*foo' with a slug that ends with foo.
'*foo*' with a slug that contains foo.
'not *foo*' with a slug that doesn’t contain foo.
['*foo*', '*bar*'] with a slug that contains foo or bar.
['not', '*foo*', '*bar*'] with a slug that doesn’t contain foo or bar.
{# Get the requested product slug from the URL #}
{% set requestedSlug = craft.app.request.getSegment(3) %}

{# Fetch the product with that slug #}
{% set product = craft.products()
  .slug(requestedSlug|literal)
  .one() %}

# status

Narrows the query results based on the products’ statuses.

Possible values include:

Value Fetches products…
'live' (default) that are live.
'pending' that are pending (enabled with a Post Date in the future).
'expired' that are expired (enabled with an Expiry Date in the past).
'disabled' that are disabled.
['live', 'pending'] that are live or pending.
{# Fetch disabled products #}
{% set products = craft.products()
  .status('disabled')
  .all() %}

# title

Narrows the query results based on the products’ titles.

Possible values include:

Value Fetches products…
'Foo' with a title of Foo.
'Foo*' with a title that begins with Foo.
'*Foo' with a title that ends with Foo.
'*Foo*' with a title that contains Foo.
'not *Foo*' with a title that doesn’t contain Foo.
['*Foo*', '*Bar*'] with a title that contains Foo or Bar.
['not', '*Foo*', '*Bar*'] with a title that doesn’t contain Foo or Bar.
{# Fetch products with a title that contains "Foo" #}
{% set products = craft.products()
  .title('*Foo*')
  .all() %}

# trashed

Narrows the query results to only products that have been soft-deleted.

{# Fetch trashed products #}
{% set products = craft.products()
  .trashed()
  .all() %}

# type

Narrows the query results based on the products’ types.

Possible values include:

Value Fetches products…
'foo' of a type with a handle of foo.
'not foo' not of a type with a handle of foo.
['foo', 'bar'] of a type with a handle of foo or bar.
['not', 'foo', 'bar'] not of a type with a handle of foo or bar.
an ProductType object of a type represented by the object.
{# Fetch products with a Foo product type #}
{% set products = craft.products()
  .type('foo')
  .all() %}

# typeId

Narrows the query results based on the products’ types, per the types’ IDs.

Possible values include:

Value Fetches products…
1 of a type with an ID of 1.
'not 1' not of a type with an ID of 1.
[1, 2] of a type with an ID of 1 or 2.
['not', 1, 2] not of a type with an ID of 1 or 2.
{# Fetch products of the product type with an ID of 1 #}
{% set products = craft.products()
  .typeId(1)
  .all() %}

# uid

Narrows the query results based on the products’ UIDs.

{# Fetch the product by its UID #}
{% set product = craft.products()
  .uid('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')
  .one() %}

# unique

Determines whether only elements with unique IDs should be returned by the query.

This should be used when querying elements from multiple sites at the same time, if “duplicate” results is not desired.

{# Fetch unique products across all sites #}
{% set products = craft.products()
  .site('*')
  .unique()
  .all() %}

# uri

Narrows the query results based on the products’ URIs.

Possible values include:

Value Fetches products…
'foo' with a URI of foo.
'foo*' with a URI that begins with foo.
'*foo' with a URI that ends with foo.
'*foo*' with a URI that contains foo.
'not *foo*' with a URI that doesn’t contain foo.
['*foo*', '*bar*'] with a URI that contains foo or bar.
['not', '*foo*', '*bar*'] with a URI that doesn’t contain foo or bar.
{# Get the requested URI #}
{% set requestedUri = craft.app.request.getPathInfo() %}

{# Fetch the product with that URI #}
{% set product = craft.products()
  .uri(requestedUri|literal)
  .one() %}

# wasCountEagerLoaded

Returns whether the query result count was already eager loaded by the query's source element.

# wasEagerLoaded

Returns whether the query results were already eager loaded by the query's source element.

# with

Causes the query to return matching products eager-loaded with related elements.

See Eager-Loading Elements (opens new window) for a full explanation of how to work with this parameter.

{# Fetch products eager-loaded with the "Related" field’s relations #}
{% set products = craft.products()
  .with(['related'])
  .all() %}

# withCustomFields

Sets whether custom fields should be factored into the query.

# Variants

This section has not been updated for Commerce 5.x.

A variant describes the individual properties of a product as an item that may be purchased.

Those properties inclue a SKU, price, and dimensions. Even if a product doesn’t appear to have any variants in the control panel, it still uses one default variant behind the scenes.

Let’s compare examples of a single-variant an multi-variant product: a paperback book and a t-shirt.

A book sold in only one format does not have meaningful variations for the customer to choose, but it would still have a specific SKU, price, weight, and dimensions. A single, implicit default variant needs to exist and that’s what would be added to the cart.

A t-shirt, on the other hand, would have at least one variant for each available color and size combination. You wouldn’t sell the t-shirt without a specific color and size, so multiple variants would be necessary. If the shirt came in “small” and “large” sizes and “red” or “blue” colors, four unique variants could exist:

  • small, red
  • small, blue
  • large, red
  • large, blue

# Variant Properties

Each variant includes the following unique properties:

Property Type Required?
SKU string
Price number
Stock number or unlimited
Allowed Qty range
Dimensions number (l × w × h)
Weight number
Related Sales relationship (Sale)

Each variant may also have any number of custom fields to allow other distinguishing traits.

Commerce does not automatically create every possible unique variant for you—that’s up to the store manager.

# Default Variant

Every product has a default variant. Whenever a product is created, a default variant will be created as well.

If a product type has multiple variants enabled, the author can choose which one should be used by default. Products that do not have multiple variants still have a default variant, but the author can’t add additional variants.

For a single-variant product, variant details are shown in a unified view with custom product fields:

Stylized illustration of a single-variant product edit page, with custom product fields in a single content pane and fields like SKU in the sidebar details

When a product supports multiple variants, the default variant will be identified in a Variants field where more variants can be added:

Stylized illustration of a multi-variant product edit page, with an additional “Variants” tab that has a Matrix-like editor for multiple variants each having their own fields like SKU

# Variant Stock

Variants can have unlimited stock or a specific quantity.

A finite stock amount will automatically be reduced whenever someone completes an order, until the stock amount reaches zero. At that point the variant’s “Available for purchase” setting won’t be changed, but zero-stock variants cannot be added to a cart.

For returns or refunds that aren’t ultimately delivered to the customer, you’ll need to either manually update product stock or use the orderStatusChange event to automate further stock adjustments.

# Querying Variants

You can fetch variants using variant queries.

{# Create a new variant query #}
{% set myVariantQuery = craft.variants() %}

Once you’ve created a variant query, you can set parameters on it to narrow down the results, and then execute it (opens new window) by calling .all(). An array of Variant (opens new window) objects will be returned.

You can also fetch only the number of items a query might return, which is better for performance when you don’t need the variant data.

{# Count all enabled variants #}
{% set myVariantCount = craft.variants()
    .status('enabled')
    .count() %}

See Element Queries (opens new window) in the Craft docs to learn about how element queries work.

# Example

We can display a specific variant by its ID in Twig by doing the following:

  1. Create a variant query with craft.variants().
  2. Set the id parameter on it.
  3. Fetch the variant with .one().
  4. Output information about the variant as HTML.
{# Get the requested variant ID from the query string #}
{% set variantId = craft.app.request.getQueryParam('id') %}

{# Create a variant query with the 'id' parameter #}
{% set myVariantQuery = craft.variants()
    .id(variantId) %}

{# Fetch the variant #}
{% set variant = myVariantQuery.one() %}

{# Make sure it exists #}
{% if not variant %}
    {% exit 404 %}
{% endif %}

{# Display the variant #}
<h1>{{ variant.title }}</h1>
<!-- ... -->

Fetching the equivalent with GraphQL could look like this:

# Fetch variant having ID = 46
{
  variants(id: 46) {
    title
  }
}

# Variant Query Parameters

Variant queries support the following parameters:

Param Description
afterPopulate Performs any post-population processing on elements.
andRelatedTo Narrows the query results to only variants that are related to certain other elements.
asArray Causes the query to return matching variants as arrays of data, rather than Variant (opens new window) objects.
cache Enables query cache for this Query.
clearCachedResult Clears the cached result (opens new window).
dateCreated Narrows the query results based on the variants’ creation dates.
dateUpdated Narrows the query results based on the variants’ last-updated dates.
fixedOrder Causes the query results to be returned in the order specified by id.
hasProduct Narrows the query results to only variants for certain products.
hasSales Narrows the query results to only variants that are on sale.
hasStock Narrows the query results to only variants that have stock.
hasUnlimitedStock Narrows the query results to only variants that have been set to unlimited stock.
height Narrows the query results based on the variants’ height dimension.
id Narrows the query results based on the variants’ IDs.
ignorePlaceholders Causes the query to return matching variants as they are stored in the database, ignoring matching placeholder elements that were set by craft\services\Elements::setPlaceholderElement() (opens new window).
inReverse Causes the query results to be returned in reverse order.
isDefault Narrows the query results to only default variants.
length Narrows the query results based on the variants’ length dimension.
limit Determines the number of variants that should be returned.
maxQty Narrows the query results based on the variants’ max quantity.
minQty Narrows the query results based on the variants’ min quantity.
offset Determines how many variants should be skipped in the results.
orderBy Determines the order that the variants should be returned in. (If empty, defaults to sortOrder ASC.)
preferSites If unique is set, this determines which site should be selected when querying multi-site elements.
prepareSubquery Prepares the element query and returns its subquery (which determines what elements will be returned).
price Narrows the query results based on the variants’ price.
product Narrows the query results based on the variants’ product.
productId Narrows the query results based on the variants’ products’ IDs.
relatedTo Narrows the query results to only variants that are related to certain other elements.
search Narrows the query results to only variants that match a search query.
site Determines which site(s) the variants should be queried in.
siteId
siteSettingsId Narrows the query results based on the variants’ IDs in the elements_sites table.
sku Narrows the query results based on the variants’ SKUs.
status
stock Narrows the query results based on the variants’ stock.
title Narrows the query results based on the variants’ titles.
trashed Narrows the query results to only variants that have been soft-deleted.
typeId Narrows the query results based on the variants’ product types, per their IDs.
uid Narrows the query results based on the variants’ UIDs.
unique Determines whether only elements with unique IDs should be returned by the query.
weight Narrows the query results based on the variants’ weight dimension.
width Narrows the query results based on the variants’ width dimension.
with Causes the query to return matching variants eager-loaded with related elements.

# afterPopulate

Performs any post-population processing on elements.

# andRelatedTo

Narrows the query results to only variants that are related to certain other elements.

See Relations (opens new window) for a full explanation of how to work with this parameter.

{# Fetch all variants that are related to myCategoryA and myCategoryB #}
{% set variants = craft.variants()
  .relatedTo(myCategoryA)
  .andRelatedTo(myCategoryB)
  .all() %}

# asArray

Causes the query to return matching variants as arrays of data, rather than Variant (opens new window) objects.

{# Fetch variants as arrays #}
{% set variants = craft.variants()
  .asArray()
  .all() %}

# cache

Enables query cache for this Query.

# clearCachedResult

Clears the cached result (opens new window).

# dateCreated

Narrows the query results based on the variants’ creation dates.

Possible values include:

Value Fetches variants…
'>= 2018-04-01' that were created on or after 2018-04-01.
'< 2018-05-01' that were created before 2018-05-01.
['and', '>= 2018-04-04', '< 2018-05-01'] that were created between 2018-04-01 and 2018-05-01.
now/today/tomorrow/yesterday that were created at midnight of the specified relative date.
{# Fetch variants created last month #}
{% set start = date('first day of last month')|atom %}
{% set end = date('first day of this month')|atom %}

{% set variants = craft.variants()
  .dateCreated(['and', ">= #{start}", "< #{end}"])
  .all() %}

# dateUpdated

Narrows the query results based on the variants’ last-updated dates.

Possible values include:

Value Fetches variants…
'>= 2018-04-01' that were updated on or after 2018-04-01.
'< 2018-05-01' that were updated before 2018-05-01.
['and', '>= 2018-04-04', '< 2018-05-01'] that were updated between 2018-04-01 and 2018-05-01.
now/today/tomorrow/yesterday that were updated at midnight of the specified relative date.
{# Fetch variants updated in the last week #}
{% set lastWeek = date('1 week ago')|atom %}

{% set variants = craft.variants()
  .dateUpdated(">= #{lastWeek}")
  .all() %}

# fixedOrder

Causes the query results to be returned in the order specified by id.

If no IDs were passed to id, setting this to true will result in an empty result set.

{# Fetch variants in a specific order #}
{% set variants = craft.variants()
  .id([1, 2, 3, 4, 5])
  .fixedOrder()
  .all() %}

# hasProduct

Narrows the query results to only variants for certain products.

Possible values include:

Value Fetches variants…
a ProductQuery (opens new window) object for products that match the query.

# hasSales

Narrows the query results to only variants that are on sale.

Possible values include:

Value Fetches variants…
true on sale
false not on sale

# hasStock

Narrows the query results to only variants that have stock.

Possible values include:

Value Fetches variants…
true with stock.
false with no stock.

# hasUnlimitedStock

Narrows the query results to only variants that have been set to unlimited stock.

Possible values include:

Value Fetches variants…
true with unlimited stock checked.
false with unlimited stock not checked.

# height

Narrows the query results based on the variants’ height dimension.

Possible values include:

Value Fetches variants…
100 with a height of 100.
'>= 100' with a height of at least 100.
'< 100' with a height of less than 100.

# id

Narrows the query results based on the variants’ IDs.

Possible values include:

Value Fetches variants…
1 with an ID of 1.
'not 1' not with an ID of 1.
[1, 2] with an ID of 1 or 2.
['not', 1, 2] not with an ID of 1 or 2.
{# Fetch the variant by its ID #}
{% set variant = craft.variants()
  .id(1)
  .one() %}

This can be combined with fixedOrder if you want the results to be returned in a specific order.

# ignorePlaceholders

Causes the query to return matching variants as they are stored in the database, ignoring matching placeholder elements that were set by craft\services\Elements::setPlaceholderElement() (opens new window).

# inReverse

Causes the query results to be returned in reverse order.

{# Fetch variants in reverse #}
{% set variants = craft.variants()
  .inReverse()
  .all() %}

# isDefault

Narrows the query results to only default variants.

{# Fetch default variants #}
{% set variants = craft.variants()
  .isDefault()
  .all() %}

# length

Narrows the query results based on the variants’ length dimension.

Possible values include:

Value Fetches variants…
100 with a length of 100.
'>= 100' with a length of at least 100.
'< 100' with a length of less than 100.

# limit

Determines the number of variants that should be returned.

{# Fetch up to 10 variants  #}
{% set variants = craft.variants()
  .limit(10)
  .all() %}

# maxQty

Narrows the query results based on the variants’ max quantity.

Possible values include:

Value Fetches variants…
100 with a maxQty of 100.
'>= 100' with a maxQty of at least 100.
'< 100' with a maxQty of less than 100.

# minQty

Narrows the query results based on the variants’ min quantity.

Possible values include:

Value Fetches variants…
100 with a minQty of 100.
'>= 100' with a minQty of at least 100.
'< 100' with a minQty of less than 100.

# offset

Determines how many variants should be skipped in the results.

{# Fetch all variants except for the first 3 #}
{% set variants = craft.variants()
  .offset(3)
  .all() %}

# orderBy

Determines the order that the variants should be returned in. (If empty, defaults to sortOrder ASC.)

{# Fetch all variants in order of date created #}
{% set variants = craft.variants()
  .orderBy('dateCreated ASC')
  .all() %}

# preferSites

If unique is set, this determines which site should be selected when querying multi-site elements.

For example, if element “Foo” exists in Site A and Site B, and element “Bar” exists in Site B and Site C, and this is set to ['c', 'b', 'a'], then Foo will be returned for Site B, and Bar will be returned for Site C.

If this isn’t set, then preference goes to the current site.

{# Fetch unique variants from Site A, or Site B if they don’t exist in Site A #}
{% set variants = craft.variants()
  .site('*')
  .unique()
  .preferSites(['a', 'b'])
  .all() %}

# prepareSubquery

Prepares the element query and returns its subquery (which determines what elements will be returned).

# price

Narrows the query results based on the variants’ price.

Possible values include:

Value Fetches variants…
100 with a price of 100.
'>= 100' with a price of at least 100.
'< 100' with a price of less than 100.

# product

Narrows the query results based on the variants’ product.

Possible values include:

Value Fetches variants…
a Product (opens new window) object for a product represented by the object.

# productId

Narrows the query results based on the variants’ products’ IDs.

Possible values include:

Value Fetches variants…
1 for a product with an ID of 1.
[1, 2] for product with an ID of 1 or 2.
['not', 1, 2] for product not with an ID of 1 or 2.

# relatedTo

Narrows the query results to only variants that are related to certain other elements.

See Relations (opens new window) for a full explanation of how to work with this parameter.

{# Fetch all variants that are related to myCategory #}
{% set variants = craft.variants()
  .relatedTo(myCategory)
  .all() %}

Narrows the query results to only variants that match a search query.

See Searching (opens new window) for a full explanation of how to work with this parameter.

{# Get the search query from the 'q' query string param #}
{% set searchQuery = craft.app.request.getQueryParam('q') %}

{# Fetch all variants that match the search query #}
{% set variants = craft.variants()
  .search(searchQuery)
  .all() %}

# site

Determines which site(s) the variants should be queried in.

The current site will be used by default.

Possible values include:

Value Fetches variants…
'foo' from the site with a handle of foo.
['foo', 'bar'] from a site with a handle of foo or bar.
['not', 'foo', 'bar'] not in a site with a handle of foo or bar.
a craft\models\Site (opens new window) object from the site represented by the object.
'*' from any site.

If multiple sites are specified, elements that belong to multiple sites will be returned multiple times. If you only want unique elements to be returned, use unique in conjunction with this.

{# Fetch variants from the Foo site #}
{% set variants = craft.variants()
  .site('foo')
  .all() %}

# siteId

# siteSettingsId

Narrows the query results based on the variants’ IDs in the elements_sites table.

Possible values include:

Value Fetches variants…
1 with an elements_sites ID of 1.
'not 1' not with an elements_sites ID of 1.
[1, 2] with an elements_sites ID of 1 or 2.
['not', 1, 2] not with an elements_sites ID of 1 or 2.
{# Fetch the variant by its ID in the elements_sites table #}
{% set variant = craft.variants()
  .siteSettingsId(1)
  .one() %}

# sku

Narrows the query results based on the variants’ SKUs.

Possible values include:

Value Fetches variants…
'foo' with a SKU of foo.
'foo*' with a SKU that begins with foo.
'*foo' with a SKU that ends with foo.
'*foo*' with a SKU that contains foo.
'not *foo*' with a SKU that doesn’t contain foo.
['*foo*', '*bar*' with a SKU that contains foo or bar.
['not', '*foo*', '*bar*'] with a SKU that doesn’t contain foo or bar.
{# Get the requested variant SKU from the URL #}
{% set requestedSlug = craft.app.request.getSegment(3) %}

{# Fetch the variant with that slug #}
{% set variant = craft.variants()
  .sku(requestedSlug|literal)
  .one() %}

# status

# stock

Narrows the query results based on the variants’ stock.

Possible values include:

Value Fetches variants…
0 with no stock.
'>= 5' with a stock of at least 5.
'< 10' with a stock of less than 10.

# title

Narrows the query results based on the variants’ titles.

Possible values include:

Value Fetches variants…
'Foo' with a title of Foo.
'Foo*' with a title that begins with Foo.
'*Foo' with a title that ends with Foo.
'*Foo*' with a title that contains Foo.
'not *Foo*' with a title that doesn’t contain Foo.
['*Foo*', '*Bar*'] with a title that contains Foo or Bar.
['not', '*Foo*', '*Bar*'] with a title that doesn’t contain Foo or Bar.
{# Fetch variants with a title that contains "Foo" #}
{% set variants = craft.variants()
  .title('*Foo*')
  .all() %}

# trashed

Narrows the query results to only variants that have been soft-deleted.

{# Fetch trashed variants #}
{% set variants = craft.variants()
  .trashed()
  .all() %}

# typeId

Narrows the query results based on the variants’ product types, per their IDs.

Possible values include:

Value Fetches variants…
1 for a product of a type with an ID of 1.
[1, 2] for product of a type with an ID of 1 or 2.
['not', 1, 2] for product of a type not with an ID of 1 or 2.

# uid

Narrows the query results based on the variants’ UIDs.

{# Fetch the variant by its UID #}
{% set variant = craft.variants()
  .uid('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')
  .one() %}

# unique

Determines whether only elements with unique IDs should be returned by the query.

This should be used when querying elements from multiple sites at the same time, if “duplicate” results is not desired.

{# Fetch unique variants across all sites #}
{% set variants = craft.variants()
  .site('*')
  .unique()
  .all() %}

# weight

Narrows the query results based on the variants’ weight dimension.

Possible values include:

Value Fetches variants…
100 with a weight of 100.
'>= 100' with a weight of at least 100.
'< 100' with a weight of less than 100.

# width

Narrows the query results based on the variants’ width dimension.

Possible values include:

Value Fetches variants…
100 with a width of 100.
'>= 100' with a width of at least 100.
'< 100' with a width of less than 100.

# with

Causes the query to return matching variants eager-loaded with related elements.

See Eager-Loading Elements (opens new window) for a full explanation of how to work with this parameter.

{# Fetch variants eager-loaded with the "Related" field’s relations #}
{% set variants = craft.variants()
  .with(['related'])
  .all() %}

# Variants

Variants are purchasables, and they represent the individual items that customers will add to their carts as line items—even if a product type is limited to a single variant. A variant is where you’ll control pricing information, tax and shipping settings, inventory, dimensions, and so on.

Commerce gives you a great deal of flexibility in designing your product catalog—but an important consideration early-on is how you want customers to discover goods in your store. Setting aside for a moment single-variant products, variants don’t get their own URLs, and are queried separately from products. Prematurely grouping items into products can make selection difficult for customers, while aggressively separating variations of a single item into products can make differentiating products more difficult for customers.

Consider these tradeoffs when building your catalog—and don’t forget that you have all of Craft’s content and relational tools at your disposal!

# Prices

Every variant has a price and a promotional price. Both base prices are defined directly on the variant, but the final price shown to a customer may be determined by other pricing rules.

Prices are defined for each store a variant is available in.

# Stock

Commerce tracks inventory at the variant level.

Inventory
Manage stock and fulfill orders with sophisticated inventory tools.

# Tax & Shipping

When creating a variant, you will select its Tax and Shipping Category.

For a Tax Category to be selectable, it must be allowed for the product type that the variant belongs to.

The Shipping Category follows similar rules, but the options are defined per-store.

Parts of this page are generated or assembled by automations. While we greatly appreciate contributions to the documentation, reporting automated content issues will allow us to fix them at the source!