KQ-1331 · PRODUCT/DETAIL
Product Detail
The buying half of a product page, where every choice shows on the piece: the pictures sit in a strip that follows the finger 1:1, rubber-bands at the ends and commits by projection on glide, and a loupe follows the pointer 1:1 to show the glaze at lens power. Choosing a colour floods the new glaze across the picture from the edge nearest the picker on drift, a size grows or settles the piece from its foot on glide, the price rolls its digits on snap, and Add to bag becomes the stepper in the same box as the fill contracts onto the count on glide while − and + scale in on snap. Colours and sizes are radio groups moved with the arrow keys, the pictures a tablist whose panel takes Enter to open the loupe and arrows to move it, and a buy bar docks at the bottom of the scroll box whenever the buy row is out of it.
ember · 12 oz · look closer, then add it
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/product-detail
Props
- product
- ProductInfo
- default defaultProduct
- The product: brand, name, price, rating, colours, sizes, pictures (render functions of a colour and size), unavailable combinations and detail sections.
- value / defaultValue
- ProductSelection
- default — / the first colour with the featured size
- Controlled or initial colour and size, as { colour, size } ids.
- onValueChange
- (selection: ProductSelection) => void
- default —
- Fires from the swatch, tile or size the visitor chose, with the new selection.
- quantity / defaultQuantity
- number
- default — / 0
- The count in the bag for the selected variant; 0 shows Add to bag.
- onQuantityChange
- (quantity: number, selection: ProductSelection) => void
- default —
- Fires from Add to bag and the stepper with the new count and the variant it is for.
- onAdd
- (selection: ProductSelection) => void | Promise<void>
- default —
- Add to bag was pressed. A promise holds the button pending; a rejection's message is shown under it.
- onNotify
- (selection: ProductSelection) => void
- default —
- Notify me was pressed on a sold-out variant.
- view / defaultView
- number
- default — / 0
- Controlled or initial picture index.
- onViewChange
- (index: number) => void
- default —
- Fires from the drag, thumbnail or key that changed the picture.
- estimate
- (postcode: string, now: number) => DeliveryEstimate | null | Promise<DeliveryEstimate | null>
- default defaultEstimate
- Answers a postcode with a delivery estimate; null means no delivery there.
- defaultPostcode
- string
- default ""
- A postcode already known; its estimate shows from the start.
- onEstimate
- (postcode: string, estimate: DeliveryEstimate | null) => void
- default —
- Fires with each checked postcode and its answer.
- now
- Date | number
- default 2 Oct 2026, 09:30 UTC
- The moment estimates count from; never read from the clock during render.
- maxQuantity
- number
- default 10
- The most one variant can have in the bag; never more than its stock.
- lens
- number
- default 2.5
- The loupe's power, 1.5 to 4.
- variants
- "swatches" | "tiles" | "chips"
- default "swatches"
- How colours are offered: glaze dots, little pictures of the piece in each colour, or named pills (sizes become pills too).
- sticky
- boolean
- default true
- Dock a buy bar at the bottom of the scroll box while the buy row is out of it, and pin the pictures beside the details.
- format
- (amount: number) => string
- default euros
- Formats an amount for display.
- addLabel
- string
- default "Add to bag"
- The buy button's words.
- status
- "ready" | "loading" | "error"
- default "ready"
- Loading draws placeholders; error offers Retry.
- onRetry
- () => void
- default —
- The Retry button of the error state.
- label
- string
- default —
- The region's accessible name when it should differ from the product's name.
- sound
- boolean
- default false
- A plip on each pick and when the loupe opens, a pop on Add to bag and the stepper.
- disabled
- boolean
- default false
- Shows the product but takes no input.
- className
- string
- default —
- Classes for the region; give it a max height and it scrolls inside itself, which is what the docked bar answers to.
| Prop | Type | Default | Description |
|---|---|---|---|
| product | ProductInfo | defaultProduct | The product: brand, name, price, rating, colours, sizes, pictures (render functions of a colour and size), unavailable combinations and detail sections. |
| value / defaultValue | ProductSelection | — / the first colour with the featured size | Controlled or initial colour and size, as { colour, size } ids. |
| onValueChange | (selection: ProductSelection) => void | — | Fires from the swatch, tile or size the visitor chose, with the new selection. |
| quantity / defaultQuantity | number | — / 0 | The count in the bag for the selected variant; 0 shows Add to bag. |
| onQuantityChange | (quantity: number, selection: ProductSelection) => void | — | Fires from Add to bag and the stepper with the new count and the variant it is for. |
| onAdd | (selection: ProductSelection) => void | Promise<void> | — | Add to bag was pressed. A promise holds the button pending; a rejection's message is shown under it. |
| onNotify | (selection: ProductSelection) => void | — | Notify me was pressed on a sold-out variant. |
| view / defaultView | number | — / 0 | Controlled or initial picture index. |
| onViewChange | (index: number) => void | — | Fires from the drag, thumbnail or key that changed the picture. |
| estimate | (postcode: string, now: number) => DeliveryEstimate | null | Promise<DeliveryEstimate | null> | defaultEstimate | Answers a postcode with a delivery estimate; null means no delivery there. |
| defaultPostcode | string | "" | A postcode already known; its estimate shows from the start. |
| onEstimate | (postcode: string, estimate: DeliveryEstimate | null) => void | — | Fires with each checked postcode and its answer. |
| now | Date | number | 2 Oct 2026, 09:30 UTC | The moment estimates count from; never read from the clock during render. |
| maxQuantity | number | 10 | The most one variant can have in the bag; never more than its stock. |
| lens | number | 2.5 | The loupe's power, 1.5 to 4. |
| variants | "swatches" | "tiles" | "chips" | "swatches" | How colours are offered: glaze dots, little pictures of the piece in each colour, or named pills (sizes become pills too). |
| sticky | boolean | true | Dock a buy bar at the bottom of the scroll box while the buy row is out of it, and pin the pictures beside the details. |
| format | (amount: number) => string | euros | Formats an amount for display. |
| addLabel | string | "Add to bag" | The buy button's words. |
| status | "ready" | "loading" | "error" | "ready" | Loading draws placeholders; error offers Retry. |
| onRetry | () => void | — | The Retry button of the error state. |
| label | string | — | The region's accessible name when it should differ from the product's name. |
| sound | boolean | false | A plip on each pick and when the loupe opens, a pop on Add to bag and the stepper. |
| disabled | boolean | false | Shows the product but takes no input. |
| className | string | — | Classes for the region; give it a max height and it scrolls inside itself, which is what the docked bar answers to. |