ecommerce / components / product-overviews
With Tiered Images
01-with-tiered-images.jsx
import { StarIcon } from '@heroicons/react/20/solid'
import { CurrencyDollarIcon, GlobeAmericasIcon } from '@heroicons/react/24/outline'
const product = {
name: 'Basic Tee',
price: '$35',
rating: 3.9,
reviewCount: 512,
href: '#',
breadcrumbs: [
{ id: 1, name: 'Women', href: '#' },
{ id: 2, name: 'Clothing', href: '#' },
],
images: [
{
id: 1,
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/product-page-01-featured-product-shot.jpg',
imageAlt: "Back of women's Basic Tee in black.",
primary: true,
},
{
id: 2,
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/product-page-01-product-shot-01.jpg',
imageAlt: "Side profile of women's Basic Tee in black.",
primary: false,
},
{
id: 3,
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/product-page-01-product-shot-02.jpg',
imageAlt: "Front of women's Basic Tee in black.",
primary: false,
},
],
colors: [
{ id: 'black', name: 'Black', classes: 'bg-gray-900 checked:outline-gray-900' },
{ id: 'heather-grey', name: 'Heather Grey', classes: 'bg-gray-400 checked:outline-gray-400' },
],
sizes: [
{ id: 'xxs', name: 'XXS', inStock: true },
{ id: 'xs', name: 'XS', inStock: true },
{ id: 's', name: 'S', inStock: true },
{ id: 'm', name: 'M', inStock: true },
{ id: 'l', name: 'L', inStock: true },
{ id: 'xl', name: 'XL', inStock: false },
],
description: `
<p>The Basic tee is an honest new take on a classic. The tee uses super soft, pre-shrunk cotton for true comfort and a dependable fit. They are hand cut and sewn locally, with a special dye technique that gives each tee it's own look.</p>
<p>Looking to stock your closet? The Basic tee also comes in a 3-pack or 5-pack at a bundle discount.</p>
`,
details: [
'Only the best materials',
'Ethically and locally made',
'Pre-washed and pre-shrunk',
'Machine wash cold with similar colors',
],
}
const policies = [
{ name: 'International delivery', icon: GlobeAmericasIcon, description: 'Get your order in 2 years' },
{ name: 'Loyalty rewards', icon: CurrencyDollarIcon, description: "Don't look at other tees" },
]
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default function Example() {
return (
<div className="bg-white">
<div className="pt-6 pb-16 sm:pb-24">
<nav aria-label="Breadcrumb" className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<ol role="list" className="flex items-center space-x-4">
{product.breadcrumbs.map((breadcrumb) => (
<li key={breadcrumb.id}>
<div className="flex items-center">
<a href={breadcrumb.href} className="mr-4 text-sm font-medium text-gray-900">
{breadcrumb.name}
</a>
<svg viewBox="0 0 6 20" aria-hidden="true" className="h-5 w-auto text-gray-300">
<path d="M4.878 4.34H3.551L.27 16.532h1.327l3.281-12.19z" fill="currentColor" />
</svg>
</div>
</li>
))}
<li className="text-sm">
<a href={product.href} aria-current="page" className="font-medium text-gray-500 hover:text-gray-600">
{product.name}
</a>
</li>
</ol>
</nav>
<div className="mx-auto mt-8 max-w-2xl px-4 sm:px-6 lg:max-w-7xl lg:px-8">
<div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
<div className="lg:col-span-5 lg:col-start-8">
<div className="flex justify-between">
<h1 className="text-xl font-medium text-gray-900">{product.name}</h1>
<p className="text-xl font-medium text-gray-900">{product.price}</p>
</div>
{/* Reviews */}
<div className="mt-4">
<h2 className="sr-only">Reviews</h2>
<div className="flex items-center">
<p className="text-sm text-gray-700">
{product.rating}
<span className="sr-only"> out of 5 stars</span>
</p>
<div className="ml-1 flex items-center">
{[0, 1, 2, 3, 4].map((rating) => (
<StarIcon
key={rating}
aria-hidden="true"
className={classNames(
product.rating > rating ? 'text-yellow-400' : 'text-gray-200',
'size-5 shrink-0',
)}
/>
))}
</div>
<div aria-hidden="true" className="ml-4 text-sm text-gray-300">
·
</div>
<div className="ml-4 flex">
<a href="#" className="text-sm font-medium text-indigo-600 hover:text-indigo-500">
See all {product.reviewCount} reviews
</a>
</div>
</div>
</div>
</div>
{/* Image gallery */}
<div className="mt-8 lg:col-span-7 lg:col-start-1 lg:row-span-3 lg:row-start-1 lg:mt-0">
<h2 className="sr-only">Images</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 lg:grid-rows-3 lg:gap-8">
{product.images.map((image) => (
<img
key={image.id}
alt={image.imageAlt}
src={image.imageSrc}
className={classNames(
image.primary ? 'lg:col-span-2 lg:row-span-2' : 'hidden lg:block',
'rounded-lg',
)}
/>
))}
</div>
</div>
<div className="mt-8 lg:col-span-5">
<form>
{/* Color picker */}
<div>
<h2 className="text-sm font-medium text-gray-900">Color</h2>
<fieldset aria-label="Choose a color" className="mt-2">
<div className="flex items-center gap-x-3">
{product.colors.map((color) => (
<div key={color.id} className="flex rounded-full outline -outline-offset-1 outline-black/10">
<input
defaultValue={color.id}
defaultChecked={color === product.colors[0]}
name="color"
type="radio"
aria-label={color.name}
className={classNames(
color.classes,
'size-8 appearance-none rounded-full forced-color-adjust-none checked:outline-2 checked:outline-offset-2 focus-visible:outline-3 focus-visible:outline-offset-3',
)}
/>
</div>
))}
</div>
</fieldset>
</div>
{/* Size picker */}
<div className="mt-8">
<div className="flex items-center justify-between">
<h2 className="text-sm font-medium text-gray-900">Size</h2>
<a href="#" className="text-sm font-medium text-indigo-600 hover:text-indigo-500">
See sizing chart
</a>
</div>
<fieldset aria-label="Choose a size" className="mt-2">
<div className="grid grid-cols-3 gap-3 sm:grid-cols-6">
{product.sizes.map((size) => (
<label
key={size.id}
aria-label={size.name}
className="group relative flex items-center justify-center rounded-md border border-gray-300 bg-white p-3 has-checked:border-indigo-600 has-checked:bg-indigo-600 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-indigo-600 has-disabled:border-gray-400 has-disabled:bg-gray-200 has-disabled:opacity-25"
>
<input
defaultValue={size.id}
defaultChecked={size === product.sizes[2]}
name="size"
type="radio"
disabled={!size.inStock}
className="absolute inset-0 appearance-none focus:outline-none disabled:cursor-not-allowed"
/>
<span className="text-sm font-medium text-gray-900 uppercase group-has-checked:text-white">
{size.name}
</span>
</label>
))}
</div>
</fieldset>
</div>
<button
type="submit"
className="mt-8 flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-base font-medium text-white hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-hidden"
>
Add to cart
</button>
</form>
{/* Product details */}
<div className="mt-10">
<h2 className="text-sm font-medium text-gray-900">Description</h2>
<div
dangerouslySetInnerHTML={{ __html: product.description }}
className="mt-4 space-y-4 text-sm/6 text-gray-500"
/>
</div>
<div className="mt-8 border-t border-gray-200 pt-8">
<h2 className="text-sm font-medium text-gray-900">Fabric & Care</h2>
<div className="mt-4">
<ul role="list" className="list-disc space-y-1 pl-5 text-sm/6 text-gray-500 marker:text-gray-300">
{product.details.map((item) => (
<li key={item} className="pl-2">
{item}
</li>
))}
</ul>
</div>
</div>
{/* Policies */}
<section aria-labelledby="policies-heading" className="mt-10">
<h2 id="policies-heading" className="sr-only">
Our Policies
</h2>
<dl className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2">
{policies.map((policy) => (
<div key={policy.name} className="rounded-lg border border-gray-200 bg-gray-50 p-6 text-center">
<dt>
<policy.icon aria-hidden="true" className="mx-auto size-6 shrink-0 text-gray-400" />
<span className="mt-4 text-sm font-medium text-gray-900">{policy.name}</span>
</dt>
<dd className="mt-1 text-sm text-gray-500">{policy.description}</dd>
</div>
))}
</dl>
</section>
</div>
</div>
</div>
</div>
</div>
)
}