ProductDetail
The product page's layout. From the lg breakpoint: gallery and content on the
left, the buy box sticky on the right across both, related sections full width
below. Narrower, the parts stack in document order: gallery, buy box, content.
Children are the ProductDetail* parts, in that order.
import { ProductDetail } from "@evolve-storefront/feature-product/product-detail";Renders inside ProductListingProvider.
Preview
Usage
<ProductDetail> <ProductDetailGallery>{gallery}</ProductDetailGallery> <ProductDetailBuyBox> <Heading level={1}>{product.name}</Heading> <VariantSelector dimensions={product.variantDimensions} toHref={toHref} /> <ProductDetailBuyAction><CartAddProductButton sku={variant.sku} /></ProductDetailBuyAction> </ProductDetailBuyBox> <ProductDetailContent> <ProductDetailAnchorNav anchors={[{ id: "overview", hasContent: true }, { id: "reviews", hasContent: reviews.total > 0, count: reviews.total }]} /> <ProductDetailSection id="overview"> <ProductHighlights attributes={variant.highlightedSpecifications} locale={locale} /> <ProductDescription><Markdown>{variant.description}</Markdown></ProductDescription> </ProductDetailSection> </ProductDetailContent></ProductDetail><ProductDetailStickyBar><CartAddProductButton sku={variant.sku} /></ProductDetailStickyBar>;Props
No props of its own.
Change the layout
Pass children to ProductDetail to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-product/product-detail.
<ProductDetail> <ProductDetailGallery>{gallery}</ProductDetailGallery> <ProductDetailBuyBox> <Heading level={1}>{product.name}</Heading> <VariantSelector dimensions={product.variantDimensions} toHref={toHref} /> <ProductDetailBuyAction><CartAddProductButton sku={variant.sku} /></ProductDetailBuyAction> </ProductDetailBuyBox> <ProductDetailContent> <ProductDetailAnchorNav anchors={[{ id: "overview", hasContent: true }, { id: "reviews", hasContent: reviews.total > 0, count: reviews.total }]} /> <ProductDetailSection id="overview"> <ProductHighlights attributes={variant.highlightedSpecifications} locale={locale} /> <ProductDescription><Markdown>{variant.description}</Markdown></ProductDescription> </ProductDetailSection> </ProductDetailContent></ProductDetail><ProductDetailStickyBar><CartAddProductButton sku={variant.sku} /></ProductDetailStickyBar>;ProductBackInStockForm"Mail me when it's back" for a sold-out variant, in place of the add button.
| Prop | Type | Description |
|---|---|---|
onValidSubmit | (input: { email: string; }) => void | Called with a valid address; the site sends |
serverErrors? | FormValidationErrors | Server-provided field errors (the mapped result of a failed mutation's
typed errors — see |
validationBehavior? | "aria" | "native" | Default |
isPending? | boolean | |
isSubscribed? | boolean | The request went through: the form gives way to a confirmation. |
error? | string | null | A form-level message, such as a failed request. |
Any other props go to the root element, like on a plain HTML element.
ProductDescriptionThe product description, clamped to --product-description-lines lines (4 by default) with "Read more" when it runs longer.
No props of its own.
ProductDetailAccessoriesWhat is sold for this variant: accessories, and spare parts apart from them, each in a row.
| Prop | Type | Description |
|---|---|---|
accessories | readonly ProductAccessory<ListingProduct>[] |
|
renderItem | ProductListingItemRenderer<ListingProduct & { unit?: string | null | undefined; }> | Renders each accessory, usually a |
title? | ReactNode | |
headingLevel? | HeadingLevel | |
id? | string |
Any other props go to the root element, like on a plain HTML element.
ProductDetailAddOnsThe buy box's add-ons, such as an extra warranty: one checkbox each, with its price.
| Prop | Type | Description |
|---|---|---|
addOns | readonly ProductAddOn[] | What the variant offers, in display order. Renders nothing when empty. |
selected | readonly string[] | The ticked SKUs, from |
onSelectedChange | (selected: string[]) => void | Hands over the new list of ticked SKUs. |
Any other props go to the root element, like on a plain HTML element.
ProductDetailBundle"Complete the look" or "Frequently bought together": the page's product with items to tick, a size to choose for an item that has sizes, and a live total with the bundle's discount on the extras.
| Prop | Type | Description |
|---|---|---|
kind | "complete-the-look" | "bought-together" | Picks the title: "Complete the look" for fashion, "Frequently bought together" otherwise. |
product | { name: string; price: Money | null; image?: { url: string; alt?: string | null | undefined; } | null | undefined; } | The page's product, always part of the bundle. |
items | readonly ProductBundleItem[] | What the bundle offers with it, in display order. Renders nothing when empty. |
selection | ProductBundleSelection | From |
discountPercent? | number | The percentage off each extra, as the merchant configured the bundle discount in the commerce engine. Only shown here; the cart applies the real discount. |
vatDisplay? | VatDisplay | The VAT basis of every price passed in, as |
action? | ReactNode | Rendered under the total, such as |
renderImage? | ((props: ProductBundleImageProps) => ReactNode) | |
id? | string | |
headingLevel? | HeadingLevel |
Any other props go to the root element, like on a plain HTML element.
ProductDetailBuyActionWraps the buy box's main action, so ProductDetailStickyBar can tell when it has scrolled out of view.
No props of its own.
ProductDetailBuyBoxTitle, price, variants and the buy action.
No props of its own.
ProductDetailContentThe decision content next to the buy box: anchor navigation, then the sections.
No props of its own.
ProductDetailDeliveryPromiseWhere and when the variant arrives: "Order before 22:00, delivered tomorrow", the carrier and the delivery cost, for the shopper's postcode, which they can change in place.
| Prop | Type | Description |
|---|---|---|
promise | ProductDeliveryPromise | null | undefined | The variant's |
onPostalCodeChange | (postalCode: string) => void | The shopper changed the postcode; the site asks for the promise again. |
timeZone | string | The store's time zone, in which "tomorrow" and the order deadline are read. |
postalCode? | string | null | The postcode the promise is for, if the shopper gave one. |
alternativesHref? | string | Where to find variants or products that can be delivered, when this one cannot. |
Any other props go to the root element, like on a plain HTML element.
ProductDetailDocumentsA variant's documents, each a download with what it is, its file kind, size and language where the catalog says.
| Prop | Type | Description |
|---|---|---|
documents | readonly ProductDocument[] |
|
title? | ReactNode | |
headingLevel? | HeadingLevel | |
id? | string |
Any other props go to the root element, like on a plain HTML element.
ProductDetailGalleryThe product's images, first on a phone and top left from lg.
No props of its own.
ProductDetailPriceThe buy box's price.
| Prop | Type | Description |
|---|---|---|
price | GrossPrice | What the variant costs now. Renders nothing when absent. |
regularPrice? | GrossPrice | The price without a sale; struck through, with the percentage, when higher. |
lowestPrice? | GrossPrice | The lowest price in the 30 days before the discount, shown under a discounted price. |
Any other props go to the root element, like on a plain HTML element.
ProductDetailPromiseOne promise under the buy action, such as free delivery, with an optional icon.
| Prop | Type | Description |
|---|---|---|
icon? | "email" | "sort" | "error" | "check" | "add" | "menu" | "search" | "time" | "filter" | "security" | "success" | "pending" | "next" | "previous" | "expand" | "co… |
Any other props go to the root element, like on a plain HTML element.
ProductDetailPromisesThe promises under the buy action, by how the variant reaches the buyer.
| Prop | Type | Description |
|---|---|---|
kind | ProductKind | null | undefined | The variant's |
physical? | ReactNode | The site's delivery and return promises, as |
digital? | ReactNode | Replaces the default digital promises: instant download, no shipping. |
Any other props go to the root element, like on a plain HTML element.
ProductDetailQuantityThe buy box's quantity, or nothing for a digital variant.
| Prop | Type | Description |
|---|---|---|
value | number | |
onValueChange | (value: number) => void | |
kind | ProductKind | null | undefined | The variant's |
disabled? | boolean | Locks the value, e.g. while the previous change saves: the buttons disable and the field turns read-only but keeps focus, so a pending save cannot blur it. |
max? | number | |
min? | number | |
step? | number | The pack size: steps go by it, and a typed value snaps to a multiple of it, with
|
onRemove? | (() => void) | At the minimum, the decrement becomes a remove button that calls this, such as removing a cart line. Without it the decrement is disabled there. |
removeLabel? | string | Accessible label for the remove button; required with |
quantityRule? | ProductQuantityRule | null | The variant's |
Any other props go to the root element, like on a plain HTML element.
ProductDetailRecentlyViewedThe products the shopper looked at before this one, newest first, from this browser's history; no request is made.
| Prop | Type | Description |
|---|---|---|
product | ListingProduct | The variant on this page: recorded as viewed, and left out of the row. |
renderItem | ProductListingItemRenderer<ListingProduct> | Renders each product, usually a |
title? | ReactNode | |
headingLevel? | HeadingLevel | |
storageKey? | string | The |
limit? | number | How many products the history keeps. |
id? | string | |
show? | number | How many to show; the history may keep more. |
Any other props go to the root element, like on a plain HTML element.
ProductDetailRelatedFull-width sections under both columns, such as related products.
No props of its own.
ProductDetailRelatedProductsThe products related to this one, in a row under the page's columns.
| Prop | Type | Description |
|---|---|---|
products | readonly ListingProduct[] |
|
renderItem | ProductListingItemRenderer<ListingProduct> | Renders each product, usually a |
title? | ReactNode | |
headingLevel? | HeadingLevel | |
id? | string |
Any other props go to the root element, like on a plain HTML element.
ProductDetailSectionA content section with a heading, the target of an anchor in ProductDetailAnchorNav.
| Prop | Type | Description |
|---|---|---|
id | (string & {}) | "overview" | "specifications" | "reviews" | "questions" | "documents" | "accessories" | "comparison" | The anchor target. A known id gets its default title. |
title? | ReactNode | |
headingLevel? | HeadingLevel |
Any other props go to the root element, like on a plain HTML element.
ProductDetailStickyBarA bar stuck to the bottom of the viewport below lg, shown once the buy box's main action has scrolled up out of view.
| Prop | Type | Description |
|---|---|---|
watch? | string | The id of the element whose scrolling out of view shows the bar. |
Any other props go to the root element, like on a plain HTML element.
ProductDetailStockThe selected variant's stock as a status tag: in stock, "Only 2 left", a pre-order with its expected date and when it is charged, or sold out with a link to the nearest variant in stock.
| Prop | Type | Description |
|---|---|---|
stock | ProductStock | null | undefined | The variant's |
alternative? | ProductStockAlternative | null | The variant's |
toHref? | ((target: { slug: string; sku: string; }) => string) | The alternative's address: the app's route. |
renderLink? | ((props: StockAlternativeLinkProps) => ReactNode) |
Any other props go to the root element, like on a plain HTML element.
ProductDetailStockActionThe buy box's action by stock: the button, or the back-in-stock form in its place when sold out.
| Prop | Type | Description |
|---|---|---|
stock | ProductStock | null | undefined | The variant's |
children | (props: { action: "add" | "pre-order"; label: string | undefined; }) => ReactNode | The add button, given the action and its label: "Pre-order" for a pre-order. |
notify | ReactNode | Shown instead of the button when sold out, such as |
ProductHighlightsThe first few highlighted specifications, each as its label and value.
| Prop | Type | Description |
|---|---|---|
attributes | readonly SpecificationAttribute[] | The variant's |
locale | string | |
limit? | number | How many to show; the page leads with three. |
Any other props go to the root element, like on a plain HTML element.
Examples
Digital ProductThe buy box follows the variant's kind.
<ProductDetailBuyBox> <VariantSelector dimensions={product.variantDimensions} toHref={toHref} /> <ProductDetailQuantity kind={variant.kind} value={quantity} onValueChange={setQuantity} /> <ProductDetailBuyAction> <CartAddProductButton sku={variant.sku} quantity={orderQuantity(variant.kind, quantity)} /> </ProductDetailBuyAction> <ProductDetailPromises kind={variant.kind} physical={<ProductDetailPromise icon="shipping">{t("delivery")}</ProductDetailPromise>} /></ProductDetailBuyBox>BusinessA business page lists its sections in its own order, specifications first, and a section the framework has no title for passes its own.
<ProductDetailAnchorNav anchors={[ { id: "specifications", hasContent: true }, { id: "documents", hasContent: documents.length > 0 }, { id: "accessories", hasContent: accessories.length > 0 }, { id: "reviews", hasContent: reviews.total > 0, count: reviews.total }, ]}/>;RelatedUnder both columns: the products this one names as related, then the ones the shopper viewed before it, from this browser.
const related = await readRelatedProducts(gqlClient, { slug });
<ProductDetailRelated> <ProductDetailRelatedProducts products={related} renderItem={(product) => <ProductCard product={product} href={productHref(product)} />} /> <ProductDetailRecentlyViewed product={variant} renderItem={(product) => <ProductCard product={product} href={productHref(product)} />} /></ProductDetailRelated>;Sticky BarThe bar appears once the buy box's action has scrolled up out of view, and goes again when it scrolls back.
// After ProductDetail, in the same scrolling parent.<ProductDetailStickyBar> <span>{variantLabel} · <Price money={variant.price} /></span> <CartAddProductButton sku={variant.sku} /></ProductDetailStickyBar>;Sticky Buy Box Ends At RelatedFrom lg the buy box sticks while the content beside it scrolls, and lets go where that row ends, so it never covers a full-width related section.
<ProductDetail> <ProductDetailGallery>…</ProductDetailGallery> <ProductDetailBuyBox>…</ProductDetailBuyBox> <ProductDetailContent>…</ProductDetailContent> <ProductDetailRelated> <ProductDetailSection id="specifications">…</ProductDetailSection> </ProductDetailRelated></ProductDetail>;
