A product may be available in different variants, e.g., a particular T-shirt comes in different sizes, and each size is a variant of that T-shirt. The notion of variants is available in Saleor out of the box. You can access it on the product via the variants
field.
In the previous step, we used the ProductByID
query to fetch details for a particular product in our store. Let's now extend this query to also include the information about the possible variants of each product. In graphql/queries/ProductByID.graphql
add the following variants
snippet:
# graphql/queries/ProductByID.graphql
query ProductByID($id: ID!) {
product(id: $id, channel: "default-channel") {
id
name
description
media {
url
}
category {
name
}
variants {
id
name
}
}
}
If you run this query for a product from the T-shirt category, you will notice that the variant names are T-shirt sizes, i.e., S, M, L, or XL.
Let's display all possible variants of a product on the page that displays the product details. This is being handled by the ProductDetails
component. So, let's update it:
// components/ProductDetails.tsx
import React from 'react';
import {
Product
} from "@/saleor/api";
import {
VariantSelector
} from '@/components';
const styles = {
columns: 'grid grid-cols-2 gap-x-10 items-start',
image: {
aspect: 'aspect-w-1 aspect-h-1 bg-white rounded',
content: 'object-center object-cover'
},
details: {
title: 'text-4xl font-bold tracking-tight text-gray-800',
category: 'text-lg mt-2 font-medium text-gray-500',
description: 'prose lg:prose-s'
}
}
interface Props {
product: Pick<Product, 'id' | 'name' | 'description' | 'thumbnail' | 'category' | 'media' | 'variants'>;
}
export const ProductDetails = ({ product }: Props) => {
return (
<div className={styles.columns}>
<div className={styles.image.aspect}>
<img
src={product?.media![0]?.url}
className={styles.image.content}
/>
</div>
<div className="space-y-8">
<div>
<h1 className={styles.details.title}>
{product?.name}
</h1>
<p className={styles.details.category}>
{product?.category?.name}
</p>
</div>
<article className={styles.details.description}>
{product?.description}
</article>
<VariantSelector variants={product?.variants || []} id={product.id} />
</div>
</div>
);
}
Let's add the export statement in components/index.ts
for VariantSelector
, so that we can directly import it from @/components
.
// components/index.ts
export { ProductCollection } from './ProductCollection';
export { Layout } from './Layout';
export { ProductElement } from './ProductElement';
export { Pagination } from './Pagination';
export { Navbar } from './Navbar';
export { ProductDetails } from './ProductDetails';
export { VariantSelector } from './VariantSelector';
With the ProductByID
query adjusted by variants
, we pass them to a newly created VariantSelector
component. This component gets a collection of variants and displays their names. In the components
folder, create another file, called
VariantSelector.tsx
:
// components/VariantSelector.tsx
import React from "react";
import { ProductVariant } from "@/saleor/api";
type Variant = Pick<ProductVariant, "id" | "name"> | null | undefined;
const styles = {
grid: "grid grid-cols-8 gap-2",
variant:
"flex justify-center border rounded-md p-3 font-semibold hover:border-blue-400",
};
interface Props {
variants: Variant[];
}
export const VariantSelector = ({ variants }: Props) => {
return (
<div className={styles.grid}>
{variants.map((variant) => {
return <a className={styles.variant}>{variant?.name}</a>;
})}
</div>
);
};
The next step is to make those variants clickable so that the user can select a particular variant of a product they want to add to their cart.
For constructing className strings conditionally let's use a tiny utility - clsx
. Go to the project's root folder in your Terminal, and run:
npm install clsx
or
pnpm add clsx
Now the VariantSelector
component can be modified:
// components/VariantSelector.tsx
import React from "react";
import Link from "next/link";
import clsx from "clsx";
import { ProductVariant } from "@/saleor/api";
type Variant = Pick<ProductVariant, "id" | "name"> | null | undefined;
const styles = {
grid: "grid grid-cols-8 gap-2",
variant: {
default:
"flex justify-center border rounded-md p-3 font-semibold hover:border-blue-400",
selected: "border-2 border-blue-300 bg-blue-300",
},
};
interface Props {
id: string;
selectedVariantID: string;
variants: Variant[];
}
export const VariantSelector = ({ variants, id, selectedVariantID }: Props) => {
return (
<div className={styles.grid}>
{variants.map((variant) => {
const isSelected = variant?.id === selectedVariantID;
return (
<Link
key={variant?.name}
href={{
pathname: "/product/[id]",
query: { variant: variant?.id, id },
}}
replace
shallow
legacyBehavior
>
<a
className={clsx(
styles.variant.default,
isSelected && styles.variant.selected
)}
>
{variant?.name}
</a>
</Link>
);
})}
</div>
);
};
The id of a selected variant is being passed-in to the VariantSelector
component as the parent component must also know its value in order to add the correct product variant to the cart. Let's slightly modify the parent component, i.e., ProductDetails
:
// components/ProductDetails.tsx
import React from 'react';
import { useRouter } from "next/router";
import {
Product
} from "@/saleor/api";
import {
VariantSelector
} from '@/components';
const styles = {
columns: 'grid grid-cols-2 gap-x-10 items-start',
image: {
aspect: 'aspect-w-1 aspect-h-1 bg-white rounded',
content: 'object-center object-cover'
},
details: {
title: 'text-4xl font-bold tracking-tight text-gray-800',
category: 'text-lg mt-2 font-medium text-gray-500',
description: 'prose lg:prose-s'
}
}
interface Props {
product: Pick<Product, 'id' | 'name' | 'description' | 'thumbnail' | 'category' | 'media' | 'variants'>;
}
export const ProductDetails = ({ product }: Props) => {
const router = useRouter();
const queryVariant = process.browser
? router.query.variant?.toString()
: undefined;
const selectedVariantID = queryVariant || product?.variants![0]!.id!;
return (
<div className={styles.columns}>
<div className={styles.image.aspect}>
<img
src={product?.media![0]?.url}
className={styles.image.content}
/>
</div>
<div className="space-y-8">
<div>
<h1 className={styles.details.title}>
{product?.name}
</h1>
<p className={styles.details.category}>
{product?.category?.name}
</p>
</div>
<article className={styles.details.description}>
{product?.description}
</article>
<VariantSelector variants={product?.variants || []} id={product.id} selectedVariantID={selectedVariantID} />
</div>
</div>
);
}