Skip to content
UI

Search docs

Search components and documentation

New
← Back to ecommerce blocks

product-detail-01

A product detail page: an image gallery with selectable thumbnails, title, rating, price and description, a size selector, a quantity stepper and an add-to-cart button whose total updates with quantity. Use as a storefront product page.

Code

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { NumberField } from "@/components/ui/number-field"
import { Rating } from "@/components/ui/rating"
import { Separator } from "@/components/ui/separator"

const GALLERY = [220, 250, 28, 160]
const SIZES = ["XS", "S", "M", "L", "XL"]
const PRICE = 129

const money = (n: number) =>
  new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(n)

const swatch = (hue: number) =>
  `linear-gradient(135deg, hsl(${hue} 55% 55%), hsl(${hue} 55% 32%))`

export function ProductDetail({
  className,
  ...props
}: React.ComponentProps<"div">) {
  const [active, setActive] = React.useState(0)
  const [size, setSize] = React.useState("M")
  const [qty, setQty] = React.useState(1)

  return (
    <div
      className={cn(
        "mx-auto grid w-full max-w-5xl gap-10 px-6 py-12 lg:grid-cols-2",
        className
      )}
      {...props}
    >
      <div className="flex flex-col gap-4">
        <div
          data-slot="product-image"
          className="aspect-square w-full rounded-xl border"
          style={{ backgroundImage: swatch(GALLERY[active]) }}
        />
        <div className="grid grid-cols-4 gap-3">
          {GALLERY.map((hue, i) => (
            <button
              key={i}
              type="button"
              onClick={() => setActive(i)}
              data-active={i === active ? "" : undefined}
              aria-label={`View image ${i + 1}`}
              className="ring-ring/50 data-active:ring-ring aspect-square rounded-lg border outline-none transition-[box-shadow] focus-visible:ring-[3px] data-active:ring-2"
              style={{ backgroundImage: swatch(hue) }}
            />
          ))}
        </div>
      </div>

      <div className="flex flex-col gap-5">
        <div className="flex flex-col gap-2">
          <Badge variant="secondary" className="w-fit">
            New
          </Badge>
          <h1 className="font-heading text-2xl font-bold tracking-tight">
            Aero Runner
          </h1>
          <div className="flex items-center gap-2">
            <Rating value={4} readOnly />
            <span className="text-muted-foreground text-sm">4.0 (128 reviews)</span>
          </div>
        </div>

        <p className="text-2xl font-semibold tabular-nums">{money(PRICE)}</p>
        <p className="text-muted-foreground text-sm leading-relaxed">
          A featherweight daily trainer with a breathable knit upper and a
          responsive foam midsole. Built for the long run and the short commute
          alike.
        </p>

        <Separator />

        <div className="flex flex-col gap-2">
          <p className="text-sm font-medium">Size</p>
          <div className="flex flex-wrap gap-2">
            {SIZES.map((s) => (
              <Button
                key={s}
                type="button"
                variant={s === size ? "default" : "outline"}
                size="sm"
                className="w-11"
                onClick={() => setSize(s)}
                aria-pressed={s === size}
              >
                {s}
              </Button>
            ))}
          </div>
        </div>

        <div className="flex flex-col gap-2">
          <p className="text-sm font-medium">Quantity</p>
          <NumberField
            value={qty}
            onValueChange={(q) => setQty(Number.isNaN(q) ? 1 : q)}
            min={1}
            max={10}
            aria-label="Quantity"
          />
        </div>

        <div className="flex gap-3">
          <Button size="lg" className="flex-1" data-slot="add-to-cart">
            Add to cart · {money(PRICE * qty)}
          </Button>
          <Button size="lg" variant="outline">
            Wishlist
          </Button>
        </div>
      </div>
    </div>
  )
}