ProductCard
Shared ProductCard component. This is a presentational component that accepts handlers via props for app-specific behavior (sidepanels, stores, navigation).
Usage
import { ProductCard, ProductCardAction, ProductCardActionProps, ProductCardActionTone, ProductCardAddToCart, ProductCardAddToCartPlacement, ProductCardAddToCartRenderProps, ProductCardCategory, ProductCardColorCount, ProductCardContent, ProductCardContextValue, ProductCardCost, ProductCardDisplayCategory, ProductCardDisplayOptions, ProductCardDragAdapter, ProductCardDragAdapterProps, ProductCardDragHandle, ProductCardExternalUrl, ProductCardHref, ProductCardImage, ProductCardImageProps, ProductCardImageRenderProps, ProductCardInternalUrl, ProductCardMeta, ProductCardMode, ProductCardProcessing, ProductCardProcessingProps, ProductCardProduct, ProductCardProductCategory, ProductCardProps, ProductCardProvider, ProductCardProviderProps, ProductCardQuickView, ProductCardQuickViewProps, ProductCardRenderAddToCart, ProductCardRenderImage, ProductCardSkeleton, ProductCardSmallSkeleton, ProductCardStatusBadge, ProductCardStatusBadgeProps, ProductCardSwatch, ProductCardSwatchAdd, ProductCardSwatchAddProps, ProductCardSwatchSelector, ProductCardSwatchSelectorProps, ProductCardTools, ProductCardToolsProps, ProductCardUrlPattern, useProductCard } from "@desk/ui/compositions/product-card";Best practices
In Next.js apps pass
renderImagewrappingnext/imageandlinkAs={Link}— the defaults are a plain<img>and<a>.Cart UI renders only when a
renderAddToCartslot is provided; keepdragAdapterreferentially stable (module-level constant).Pass
renderImagewrappingnext/imagein Next.js apps — the default renderer is a plain<img>and skips Next's optimization.Keep
dragAdapterreferentially stable (a module-level constant) — an inline factory remounts the image subtree and drops the drag ref mid-drag.
Props
ProductCard
| Prop | Type | Default | Description |
|---|---|---|---|
| width | number | undefined | Fixed width in pixels for image sizing hints. When omitted, card fills its container (fluid width). | |
| mode | ProductCardMode | undefined | Mode determines the card behavior:
- "single": Shows a single product (no carousel, swatches)
- "group": Shows a product group with carousel, swatches, etc.
- "inspiration": Image-forward card with hover overlay (for
explore/masonry feeds)
- "swatch": Compact Material Desk tile (Figma Material Card) — image-first
idle; hover / keyboard-focus-visible chrome (brand/name + Quickview +
spark); selected (not hovered) = shell / selection border only (no
spark); | |
| active | boolean | undefined | Whether this card is active (highlighted). Apps should compute this based on their own state (e.g., sidepanel open, recommendations anchor). | |
| dragging | boolean | undefined | Swatch-mode canvas drag: when true, hide Quickview / brand-name caption / spark while keeping the selected shell (white border). Distinct from {@link dragAdapter} (catalog dnd-kit wiring). | |
| renderImage | ProductCardRenderImage | undefined | Render slot for framework-specific image components. | |
| onAdd | (() => void) | undefined | Swatch-mode spark button handler. The control mounts in swatch mode and
reveals on hover / focus-visible / | |
| action | ProductCardActionProps | undefined | Optional flexible "action notice" shown under the meta row (e.g. ⚡ Ships overnight, ⌕ Find similar). Prop-driven; omit to hide. | |
| dragAdapter | ProductCardDragAdapter | undefined | Component slot that wires drag behavior (e.g. dnd-kit | |
| linkAs | ElementType | undefined | Component to render internal navigation links.
Defaults to | |
| linkPrefetch | boolean | undefined | Controls | |
| maxSwatches | number | undefined | ||
| menuOpen | boolean | undefined | When true, the swatch spark spins to a Minus while an anchored suggestions
menu is open. Pair with | |
| addGlyph | ProductCardSwatchAddGlyph | undefined | Collapsed glyph for the swatch add control. | |
| onCardClick | (() => void) | undefined | Override the default card click behavior. Apps should provide this to handle navigation/sidepanel opening. | |
| onQuickView | (() => void) | undefined | When provided, a "Quickview" glass pill appears over the image on hover and invokes this handler (e.g. open a quick-view sidepanel). Prop-driven; omit to hide. | |
| onVariantChange | ((variantId: string) => void) | undefined | Callback when variant selection changes. | |
| processing | ProductCardProcessingProps | undefined | Swatch-mode processing footer ("Matching materials" + elapsed). When set, replaces the brand/name caption and hides Quickview. | |
| product | ProductCardProduct | ||
| productUrlPattern | ProductCardUrlPattern | undefined | "/products/{slug}" | URL pattern for product links. Use {slug} as placeholder. |
| renderAddToCart | ProductCardRenderAddToCart | undefined | Render slot for the add-to-cart control, placement-discriminated ("compact" | "cta" | "inspiration"). Omit to render no cart UI at all. | |
| renderTools | ((props: { actionButtonSize: ButtonProps["size"]; productId: string; variantId: string; }) => ReactNode) | undefined | Render slot for app-specific tools (saves, visual search, etc.) Rendered in the image overlay area. | |
| showActivePointer | boolean | undefined | Show the active row pointer under the card when active. | |
| statusBadge | ProductCardStatusBadgeProps | undefined | Optional status badge overlaid on the image (e.g. "New"). Prop-driven; omit to hide. | |
| variantId | string | undefined | For "single" mode: the specific variant ID to display For "group" mode: the initially selected variant ID | |
| overrideImageUrl | string | undefined | Override image URL - used for visual search to show the matched image | |
| priority | boolean | undefined | Prioritise loading images (sets loading="eager" and adds preload link) | |
| showAddToCart | boolean | undefined | ||
| showProductBrand | boolean | undefined | ||
| showProductName | boolean | undefined | ||
| showSwatches | boolean | undefined | ||
| similarityScore | number | undefined |
ProductCardAction
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | ||
| href | string | When set, renders the notice as a link (e.g. "Find similar"). | |
| icon | import("../../tokens/icon-component").IconComponent | Optional leading glyph (16px). Pass any icon from | |
| label | ReactNode | The notice text. When omitted, the component renders | |
| onClick | () => void | When set, renders the notice as a button. Takes precedence over | |
| tone | ProductCardActionTone | "muted" | Color role for the label + icon. |
ProductCardAddToCart
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | undefined | ||
| onAdd | (() => void) | undefined | Callback after item is successfully added to cart |
ProductCardContent
| Prop | Type | Default | Description |
|---|---|---|---|
| width | number | undefined | Fixed width in pixels for image sizing hints. When omitted, card fills its container (fluid width). | |
| dragging | boolean | undefined | Swatch-mode canvas drag: when true, hide Quickview / brand-name caption / spark while keeping the selected shell (white border). Distinct from {@link dragAdapter} (catalog dnd-kit wiring). | |
| action | ProductCardActionProps | undefined | Optional flexible "action notice" shown under the meta row (e.g. ⚡ Ships overnight, ⌕ Find similar). Prop-driven; omit to hide. | |
| linkAs | ElementType | undefined | Component to render internal navigation links.
Defaults to | |
| linkPrefetch | boolean | undefined | Controls | |
| menuOpen | boolean | undefined | When true, the swatch spark spins to a Minus while an anchored suggestions
menu is open. Pair with | |
| addGlyph | ProductCardSwatchAddGlyph | undefined | Collapsed glyph for the swatch add control. | |
| onCardClick | (() => void) | undefined | Override the default card click behavior. Apps should provide this to handle navigation/sidepanel opening. | |
| onQuickView | (() => void) | undefined | When provided, a "Quickview" glass pill appears over the image on hover and invokes this handler (e.g. open a quick-view sidepanel). Prop-driven; omit to hide. | |
| showActivePointer | boolean | undefined | Show the active row pointer under the card when active. | |
| statusBadge | ProductCardStatusBadgeProps | undefined | Optional status badge overlaid on the image (e.g. "New"). Prop-driven; omit to hide. | |
| overrideImageUrl | string | undefined | Override image URL - used for visual search to show the matched image | |
| priority | boolean | undefined | Prioritise loading images (sets loading="eager" and adds preload link) | |
| showAddToCart | boolean | undefined | ||
| showProductBrand | boolean | undefined | ||
| showProductName | boolean | undefined | ||
| similarityScore | number | undefined | ||
| onAddToCartSuccess | (() => void) | undefined |
ProductCardImage
| Prop | Type | Default | Description |
|---|---|---|---|
| overrideImageUrl | string | undefined | Override image URL - used for visual search to show the matched image | |
| priority | boolean | undefined | Prioritise loading this image (sets loading="eager" and adds preload link) | |
| showSimilarityScore | boolean | undefined | ||
| similarityScore | number | undefined |
ProductCardProcessing
| Prop | Type | Default | Description |
|---|---|---|---|
| elapsed | string | undefined | Right-aligned elapsed / ETA label (e.g. | |
| label | string | undefined | "Matching materials" | Process chip label. |
| onClick | (() => void) | undefined | Optional click on the process chip. |
ProductCardProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| action | ProductCardActionProps | undefined | Optional flexible "action notice" shown under the meta row (e.g. ⚡ Ships overnight, ⌕ Find similar). Prop-driven; omit to hide. | |
| children | ReactNode | ||
| display | ProductCardDisplayOptions | undefined | ||
| dragAdapter | ProductCardDragAdapter | undefined | Component slot that wires drag behavior (e.g. dnd-kit | |
| active | boolean | undefined | Whether this card is active (highlighted). Apps should compute this based on their own state (e.g., sidepanel open, recommendations anchor). | |
| dragging | boolean | undefined | Swatch-mode canvas drag: when true, hide Quickview / brand-name caption / spark while keeping the selected shell (white border). Distinct from {@link dragAdapter} (catalog dnd-kit wiring). | |
| linkAs | ElementType | undefined | Component to render internal navigation links.
Defaults to | |
| linkPrefetch | boolean | undefined | Controls | |
| maxSwatches | number | undefined | ||
| mode | ProductCardMode | undefined | Mode determines the card behavior:
- "single": Shows a single product (no carousel, swatches)
- "group": Shows a product group with carousel, swatches, etc.
- "inspiration": Image-forward card with hover overlay (for
explore/masonry feeds)
- "swatch": Compact Material Desk tile (Figma Material Card) — image-first
idle; hover / keyboard-focus-visible chrome (brand/name + Quickview +
spark); selected (not hovered) = shell / selection border only (no
spark); | |
| onAdd | (() => void) | undefined | Swatch-mode spark button handler. The control mounts in swatch mode and
reveals on hover / focus-visible / | |
| addGlyph | ProductCardSwatchAddGlyph | undefined | Collapsed glyph for the swatch add control. | |
| menuOpen | boolean | undefined | When true, the swatch spark spins to a Minus while an anchored suggestions
menu is open. Pair with | |
| onCardClick | (() => void) | undefined | Override the default card click behavior. Apps should provide this to handle navigation/sidepanel opening. | |
| onQuickView | (() => void) | undefined | When provided, a "Quickview" glass pill appears over the image on hover and invokes this handler (e.g. open a quick-view sidepanel). Prop-driven; omit to hide. | |
| processing | ProductCardProcessingProps | undefined | Swatch-mode processing footer ("Matching materials" + elapsed). When set, replaces the brand/name caption and hides Quickview. | |
| onVariantChange | ((variantId: string) => void) | undefined | Callback when variant selection changes. | |
| product | ProductCardProduct | ||
| productUrlPattern | ProductCardUrlPattern | undefined | "/products/{slug}" | URL pattern for product links. Use {slug} as placeholder. |
| renderAddToCart | ProductCardRenderAddToCart | undefined | Render slot for the add-to-cart control, placement-discriminated ("compact" | "cta" | "inspiration"). Omit to render no cart UI at all. | |
| renderImage | ProductCardRenderImage | undefined | Render slot for framework-specific image components. | |
| renderTools | ((props: { actionButtonSize: ButtonProps["size"]; productId: string; variantId: string; }) => ReactNode) | undefined | Render slot for app-specific tools (saves, visual search, etc.) Rendered in the image overlay area. | |
| showActivePointer | boolean | undefined | Show the active row pointer under the card when active. | |
| statusBadge | ProductCardStatusBadgeProps | undefined | Optional status badge overlaid on the image (e.g. "New"). Prop-driven; omit to hide. | |
| variantId | string | undefined | For "single" mode: the specific variant ID to display For "group" mode: the initially selected variant ID | |
| width | number | undefined | Fixed width in pixels for image sizing hints. When omitted, card fills its container (fluid width). |
ProductCardQuickView
| Prop | Type | Default | Description |
|---|---|---|---|
| align | "center" | "start" | undefined | Horizontal placement over the image.
- | |
| className | string | ||
| forceVisible | boolean | undefined | Keep the pill visible without hover (e.g. Storybook forced state). Swatch selection does not use this — Quickview stays hover / keyboard-focus-visible gated (not mouse :focus-within after click). | |
| label | string | "Quickview" | Pill label. |
| onQuickView | (() => void) | undefined | Invoked when the pill is activated. When omitted, nothing renders. |
ProductCardSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string |
ProductCardSmallSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | ||
| style | CSSProperties |
ProductCardStatusBadge
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | Badge text. When omitted, nothing renders. | |
| tone | "neutral" | "brand" | "danger" | "warning" | "success" | "info" | "brand" (the yellow "New" badge). | Badge tone. |
ProductCardSwatch
| Prop | Type | Default | Description |
|---|---|---|---|
| overrideImageUrl | string | undefined | ||
| priority | boolean | undefined |
ProductCardSwatchAdd
| Prop | Type | Default | Description |
|---|---|---|---|
| active | boolean | undefined | Selected / active → brand yellow when the control is revealed; does not force visibility on its own. | |
| className | string | undefined | ||
| wrapperClassName | string | undefined | Overrides the outer anchor ( | |
| menuOpen | boolean | undefined | When true, the glyph spins from Sparkle to Minus, brand chrome stays on, and the control stays visible even without hover — so the close affordance remains reachable while the suggestions panel is open. | |
| revealed | boolean | undefined | Parent pointer-hover has already revealed chrome. Kept separate from
| |
| onAdd | (() => void) | undefined | Click handler for the spark. When omitted, the control stays visible but is inert (no pointer/keyboard capture) so parent surfaces can own selection. | |
| glyph | ProductCardSwatchAddGlyph | undefined | Collapsed glyph. |
ProductCardSwatchSelector
| Prop | Type | Default | Description |
|---|---|---|---|
| size | SwatchSize | Swatch size, inherited by items via context unless they override it. |
ProductCardTools
| Prop | Type | Default | Description |
|---|---|---|---|
| active | boolean | ||
| className | string |
Also exports
- interface
ProductCardActionProps - type
ProductCardActionTone - type
ProductCardAddToCartPlacement - interface
ProductCardAddToCartRenderProps - component
ProductCardCategory - component
ProductCardColorCount - interface
ProductCardContextValue - component
ProductCardCost - interface
ProductCardDisplayCategory - interface
ProductCardDisplayOptions - type
ProductCardDragAdapter - interface
ProductCardDragAdapterProps - interface
ProductCardDragHandle - type
ProductCardExternalUrl - type
ProductCardHref - interface
ProductCardImageProps - interface
ProductCardImageRenderProps - type
ProductCardInternalUrl - component
ProductCardMeta - type
ProductCardMode - interface
ProductCardProcessingProps - interface
ProductCardProduct - interface
ProductCardProductCategory - type
ProductCardProps - interface
ProductCardProviderProps - interface
ProductCardQuickViewProps - type
ProductCardRenderAddToCart - type
ProductCardRenderImage - interface
ProductCardStatusBadgeProps - interface
ProductCardSwatchAddProps - type
ProductCardSwatchSelectorProps - interface
ProductCardToolsProps - type
ProductCardUrlPattern - const
useProductCard
Composition
Builds on
Badge, Box, Button, Center, ExpandButton, Icon, MarqueeText, MediaFrame, Popover, Skeleton, Stack, SwatchSelector, Text, Tooltip
Used by