Skip to content

VariantSelector

Every dimension of a product, each as the selector its options call for.

import { VariantSelector } from "@evolve-storefront/feature-product/variant-selector";

Renders inside ProductListingProvider.

Preview

DefaultOpen in Storybook ↗

Usage

<VariantSelector
dimensions={product.variantDimensions}
toHref={({ slug, sku }) => productDetailHref(slug, sku)}
renderLink={({ children, ...props }) => <Link {...props}>{children}</Link>}
renderImage={(props) => <Image {...props} width={64} height={64} />}
/>;

Props

PropTypeDescription
toHref(target: VariantTarget) => string

The app's product route: ({ slug, search }) => ${productDetailHref(slug)}?${search}``.

dimensionsreadonly VariantDimension[]
renderLink?((props: VariantLinkProps) => ReactNode)
renderHelp?((dimension: VariantDimension) => ReactNode)

Shown under the legend, such as a size chart's trigger or a fit line.

renderImage?((props: VariantImageProps) => ReactNode)

Any other props go to the root element, like on a plain HTML element.

Change the layout

Pass children to VariantSelector to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-product/variant-selector.

ButtonSelectorA dimension's options as labelled buttons, such as sizes.
PropTypeDescription
dimensionVariantDimension
toHref(target: VariantTarget) => string

The app's product route: ({ slug, search }) => ${productDetailHref(slug)}?${search}``.

dimensions?readonly VariantDimension[]

Every dimension of the product, so an option's search keeps what the others have selected. Defaults to this dimension alone.

renderLink?((props: VariantLinkProps) => ReactNode)
renderHelp?((dimension: VariantDimension) => ReactNode)

Shown under the legend, such as a size chart's trigger or a fit line.

Any other props go to the root element, like on a plain HTML element.

ChooseVariantButtonStands in for the add button while a dimension is unchosen ("Choose your size").
PropTypeDescription
dimensionPick<VariantDimension, "label" | "name">

The dimension still to choose: the first of variantSelection's unchosen.

variant?ButtonVariant
tone?ButtonTone
isIconOnly?boolean

The button has no visible label, so it is square. Structural rather than a size: it says the button has nothing to be wide for, and says nothing about how big — the brand's control tokens still decide that.

More

Not inferred from the children. A kebab menu is as often a glyph or an SVG as an <Icon>, and a visually-hidden label — the accessible thing to add — would break any :only-child predicate silently.

asChild?boolean
isPending?boolean

The action is running. The button keeps its label, so its width and accessible name hold, shows a spinner and is aria-busy. It stays focusable rather than disabled, so focus is not dropped mid-save; clicks and the form submit it would trigger are ignored. Not supported with asChild.

data-component?undefined

A button's identity is not a caller's to rename — see buttonProps. Typed away so a call site renaming it fails to compile rather than passing an attribute that is silently discarded.

Any other props go to the root element, like on a plain HTML element.

ColorSelectorA colour dimension as swatches, coloured by each option's color only.
PropTypeDescription
dimensionVariantDimension
toHref(target: VariantTarget) => string

The app's product route: ({ slug, search }) => ${productDetailHref(slug)}?${search}``.

dimensions?readonly VariantDimension[]

Every dimension of the product, so an option's search keeps what the others have selected. Defaults to this dimension alone.

renderLink?((props: VariantLinkProps) => ReactNode)
renderHelp?((dimension: VariantDimension) => ReactNode)

Shown under the legend, such as a size chart's trigger or a fit line.

Any other props go to the root element, like on a plain HTML element.

ImageSelectorA dimension as images of its variants, falling back to the label where one has none.
PropTypeDescription
dimensionVariantDimension
toHref(target: VariantTarget) => string

The app's product route: ({ slug, search }) => ${productDetailHref(slug)}?${search}``.

dimensions?readonly VariantDimension[]

Every dimension of the product, so an option's search keeps what the others have selected. Defaults to this dimension alone.

renderLink?((props: VariantLinkProps) => ReactNode)
renderHelp?((dimension: VariantDimension) => ReactNode)

Shown under the legend, such as a size chart's trigger or a fit line.

renderImage?((props: VariantImageProps) => ReactNode)

Any other props go to the root element, like on a plain HTML element.

Examples

Choose A SizeNo size until the shopper picks one: variantSelection clears the server's default, and ChooseVariantButton stands in for the add button.
const { choice, sku } = await readVariantChoiceForProduct(serverClient, { slug, params });
const product = await getProduct(slug, sku); // variantDimensions(selectedSku: sku)
const { dimensions, unchosen } = variantSelection(product.variantDimensions, { choice });
<VariantSelector
dimensions={dimensions}
toHref={({ slug, search }) => `${productDetailHref(slug)}?${search}`}
renderLink={(props) => <Link {...props} />}
renderHelp={(dimension) => dimension.name === "size" ? <SizeChartSheet /> : null}
/>
{unchosen[0] ? <ChooseVariantButton dimension={unchosen[0]} /> : <AddToCartButton sku={sku} />}
Open in Storybook ↗
Price DifferencesOptions that cost different amounts show the difference against the selected one, and with the cheapest selected a line names the next one up.
<VariantSelector
dimensions={product.variantDimensions} // options select variant { price { gross } }
toHref={({ slug, search }) => `${productDetailHref(slug)}?${search}`}
/>
Open in Storybook ↗
Sold Out ChosenA sold-out size the shopper has chosen: struck through on the chosen fill, in the chosen state's own text colour so it keeps its contrast.
<VariantSelector
dimensions={product.variantDimensions} // options select variant { stock { level } }
toHref={({ slug, search }) => `${productDetailHref(slug)}?${search}`}
/>
Open in Storybook ↗
Fit From ReviewsHow the size fits by the reviews, under the size legend: ProductReviewsFit in renderHelp, fed the product's reviewFitDistribution.
<VariantSelector
dimensions={product.variantDimensions}
toHref={({ slug, search }) => `${productDetailHref(slug)}?${search}`}
renderHelp={(dimension) => dimension.name === "size" ? (
<ProductReviewsFit fitDistribution={product.reviewFitDistribution} />
) : null}
/>
Open in Storybook ↗