Skip to main content

Show available variants

This guide shows you how to render a product form that includes a list of available product variants.



Many product pages include a form where options are submitted to the server when the user adds to the cart. The most basic product form enables customers to select from available variants. For example, variants might include product size and color.

An image of a product form, including variants for t-shirt color and size.

In Hydrogen we recommend using a Link to select each variant. This automatically updates the URL when customers select a variant, which has the following benefits:

  • Search engines can easily index each separate variant.
  • Users can share and bookmark each separate variant.
  • Variants can be prefetched on hover, which decreases the perceived load time.

Anchor to Query the Storefront API for Product OptionsQuery the Storefront API for Product Options

First, your product query needs to include product options. Do this by adding options with the name and optionValues properties:

JavaScript

routes/products.$handle.jsx
export async function loader({ request, params }) {
const { product } = await context.storefront.query(PRODUCT_QUERY, {
variables: {
handle: params.productHandle,
},
});

return json({ product });
}

const PRODUCT_QUERY = `#graphql
query Product(
$handle: String!
) {
product(handle: $handle) {
id
title
vendor
handle
descriptionHtml
description
options {
name
optionValues {
name
}
}
media(first: 7) {
nodes {
...Media
}
}
seo {
description
title
}
}
}
`;

Anchor to VariantSelector componentVariantSelector component

Now that you've queried product options, you can use the VariantSelector component to render links for all product options:

JavaScript

routes/products.$handle.jsx
import { VariantSelector } from "@shopify/hydrogen";

const ProductForm = ({ product }) => {
return (
<VariantSelector options={product.options}>
{({ option }) => (
<>
<div>{option.name}</div>
<div>
{option.values.map(({ value, to, isActive }) => (
<link
to={to}
// Including prefetch means if the user hovers over the link, then
// React Router fetches the page in the background
prefetch="intent"
className={isActive ? "active" : ""}
/>
{value}
))}
</div>
)}
</VariantSelector>
);
};

Anchor to Calculating the selected productCalculating the selected product

To calculate the selected product options based on URL parameters, update your GraphQL query to use the variantBySelectedOptions and use getSelectedProductOptions:

JavaScript

routes/products.$handle.jsx
import { getSelectedProductOptions } from "@shopify/hydrogen";

export async function loader({ request, params }) {
const selectedOptions = getSelectedProductOptions(request);

const { product } = await context.storefront.query(PRODUCT_QUERY, {
variables: {
handle: params.productHandle,
selectedOptions,
},
});

return json({ product });
}

const PRODUCT_QUERY = `#graphql
query Product(
$handle: String!
$selectedOptions: [SelectedOptionInput!]!
) {
product(handle: $handle) {
id
title
vendor
handle
descriptionHtml
description
options {
name
optionValues {
name
}
}
selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
...ProductVariantFragment
}
media(first: 7) {
nodes {
...Media
}
}
seo {
description
title
}
}
}
`;

Anchor to Product Variants by AvailabilityProduct Variants by Availability

Sometimes you might want to render the variants differently based on product availability. You can do this by adding product variants to your Storefront API query.

Add variants to the product query and include the availableForSale property. Because variants is a list, you have to decide how many items to query. You can include all items, but for efficiency we recommend doing this if you only have a handful to query. For example, querying all if you have one hundred variants can have a negative impact on performance.

JavaScript

routes/products.$handle.jsx
import {
VariantSelector,
getSelectedProductOptions,
} from "@shopify/hydrogen";

export async function loader({ request, params }) {
const selectedOptions = getSelectedProductOptions(request);

const { product } = await context.storefront.query(PRODUCT_QUERY, {
variables: {
handle: params.productHandle,
selectedOptions,
},
});

return json({ product });
}

const ProductForm = ({ product }) => {
return (
<VariantSelector options={product.options}>
{({ option }) => (
<>
<div>{option.name}</div>
<div>
{option.values.map(({ value, isAvailable, to, isActive }) => (
<link
to={to}
prefetch="intent"
className={
isActive ? "active" : isAvailable ? "" : "opacity-80"
}
/>
{value}
))}
</div>
)}
</VariantSelector>
);
};

const PRODUCT_QUERY = `#graphql
query Product(
$handle: String!
$selectedOptions: [SelectedOptionInput!]!
) {
product(handle: $handle) {
id
title
vendor
handle
descriptionHtml
description
options {
name
optionValues {
name
}
}
selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
...ProductVariantFragment
}
variants(first: 10) {
nodes {
...ProductVariantFragment
}
}
media(first: 7) {
nodes {
...Media
}
}
seo {
description
title
}
}
}
`;

Use the CartForm component to add the selectedVariant to the cart. You can also disable the add to cart button if the selected variant is invalid or unavailable:

JavaScript

routes/products.$handle.jsx
import {
VariantSelector,
getSelectedProductOptions,
CartForm,
} from "@shopify/hydrogen";

export async function loader({ request, params }) {
const selectedOptions = getSelectedProductOptions(request);

const { product } = await context.storefront.query(PRODUCT_QUERY, {
variables: {
handle: params.productHandle,
selectedOptions,
},
});

return json({ product });
}

const ProductForm = ({ product }) => {
return (
<>
<VariantSelector
options={product.options}
variants={product.variants}
>
{({ option }) => (
<>
<div>{option.name}</div>
<div>
{option.values.map(({ value, isAvailable, to, isActive }) => (
<link
to={to}
prefetch="intent"
className={
isActive ? "active" : isAvailable ? "" : "opacity-80"
}
/>
{value}
))}
</div>
)}
</VariantSelector>
<CartForm
route="/cart"
action={CartForm.ACTIONS.LinesAdd}
lines={[
{
merchandiseId: product.selectedVariant?.id,
},
]}
>
<button
disabled={
!product.selectedVariant?.id ||
!product.selectedVariant?.availableForSale
}
>
Add to Cart
</button>
</CartForm>
);
};

const PRODUCT_QUERY = `#graphql
query Product(
$handle: String!
$selectedOptions: [SelectedOptionInput!]!
) {
product(handle: $handle) {
id
title
vendor
handle
descriptionHtml
description
options {
name
optionValues {
name
}
}
selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
...ProductVariantFragment
}
variants(first: 10) {
nodes {
...ProductVariantFragment
}
}
media(first: 7) {
nodes {
...Media
}
}
seo {
description
title
}
}
}
`;

Anchor to Automatically select a default variantAutomatically select a default variant

You may want to automatically have a variant selected when the page first loads. We recommend selecting a default variant by redirecting to a variant from the loader:

JavaScript

routes/products.$handle.jsx
import {
VariantSelector,
getSelectedProductOptions,
CartForm,
} from "@shopify/hydrogen";

export async function loader({ request, params }) {
const selectedOptions = getSelectedProductOptions(request);

const { product } = await context.storefront.query(PRODUCT_QUERY, {
variables: {
handle: params.productHandle,
selectedOptions,
},
});

if (!product.selectedVariant) {
const searchParams = new URLSearchParams(new URL(request.url).search);
const firstVariant = product.variants.nodes[0];

for (const option of firstVariant.selectedOptions) {
searchParams.set(option.name, option.value);
}

throw redirect(
`/products/${product!.handle}?${searchParams.toString()}`,
302, // Make sure to use a 302, because the first variant is subject to change
);
}

return json({ product });
}

const ProductForm = ({ product }) => {
return (
<>
<VariantSelector
options={product.options}
variants={product.variants}
>
{({ option }) => (
<>
<div>{option.name}</div>
<div>
{option.values.map(({ value, isAvailable, to, isActive }) => (
<link
to={to}
prefetch="intent"
className={
isActive ? "active" : isAvailable ? "" : "opacity-80"
}
/>
{value}
))}
</div>
)}
</VariantSelector>
<CartForm
route="/cart"
action={CartForm.ACTIONS.LinesAdd}
lines={[
{
merchandiseId: product.selectedVariant?.id,
},
]}
>
<button
disabled={
!product.selectedVariant?.id ||
!product.selectedVariant?.availableForSale
}
>
Add to Cart
</button>
</CartForm>
);
};

const PRODUCT_QUERY = `#graphql
query Product(
$handle: String!
$selectedOptions: [SelectedOptionInput!]!
) {
product(handle: $handle) {
id
title
vendor
handle
descriptionHtml
description
options {
name
optionValues {
name
}
}
selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
...ProductVariantFragment
}
variants(first: 10) {
nodes {
...ProductVariantFragment
}
}
media(first: 7) {
nodes {
...Media
}
}
seo {
description
title
}
}
}
`;


Was this page helpful?