ecommerce / components / product-quickviews
With Color And Size Selector
02-with-color-and-size-selector.jsx
'use client'
import { useState } from 'react'
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
import { XMarkIcon } from '@heroicons/react/24/outline'
import { StarIcon } from '@heroicons/react/20/solid'
const product = {
name: 'Basic Tee 6-Pack',
price: '$192',
rating: 3.9,
reviewCount: 117,
href: '#',
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/product-quick-preview-02-detail.jpg',
imageAlt: 'Two each of gray, white, and black shirts arranged on table.',
colors: [
{ id: 'white', name: 'White', classes: 'bg-white checked:outline-gray-400' },
{ id: 'gray', name: 'Gray', classes: 'bg-gray-200 checked:outline-gray-400' },
{ id: 'black', name: 'Black', classes: 'bg-gray-900 checked:outline-gray-900' },
],
sizes: [
{ name: 'XXS', inStock: true },
{ name: 'XS', inStock: true },
{ name: 'S', inStock: true },
{ name: 'M', inStock: true },
{ name: 'L', inStock: true },
{ name: 'XL', inStock: true },
{ name: 'XXL', inStock: true },
{ name: 'XXXL', inStock: false },
],
}
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default function Example() {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onClose={setOpen} className="relative z-10">
<DialogBackdrop
transition
className="fixed inset-0 hidden bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in md:block"
/>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-stretch justify-center text-center md:items-center md:px-2 lg:px-4">
<DialogPanel
transition
className="flex w-full transform text-left text-base transition data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in md:my-8 md:max-w-2xl md:px-4 data-closed:md:translate-y-0 data-closed:md:scale-95 lg:max-w-4xl"
>
<div className="relative flex w-full items-center overflow-hidden bg-white px-4 pt-14 pb-8 shadow-2xl sm:px-6 sm:pt-8 md:p-6 lg:p-8">
<button
type="button"
onClick={() => setOpen(false)}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-500 sm:top-8 sm:right-6 md:top-6 md:right-6 lg:top-8 lg:right-8"
>
<span className="sr-only">Close</span>
<XMarkIcon aria-hidden="true" className="size-6" />
</button>
<div className="grid w-full grid-cols-1 items-start gap-x-6 gap-y-8 sm:grid-cols-12 lg:gap-x-8">
<img
alt={product.imageAlt}
src={product.imageSrc}
className="aspect-2/3 w-full rounded-lg bg-gray-100 object-cover sm:col-span-4 lg:col-span-5"
/>
<div className="sm:col-span-8 lg:col-span-7">
<h2 className="text-2xl font-bold text-gray-900 sm:pr-12">{product.name}</h2>
<section aria-labelledby="information-heading" className="mt-2">
<h3 id="information-heading" className="sr-only">
Product information
</h3>
<p className="text-2xl text-gray-900">{product.price}</p>
{/* Reviews */}
<div className="mt-6">
<h4 className="sr-only">Reviews</h4>
<div className="flex items-center">
<div className="flex items-center">
{[0, 1, 2, 3, 4].map((rating) => (
<StarIcon
key={rating}
aria-hidden="true"
className={classNames(
product.rating > rating ? 'text-gray-900' : 'text-gray-200',
'size-5 shrink-0',
)}
/>
))}
</div>
<p className="sr-only">{product.rating} out of 5 stars</p>
<a href="#" className="ml-3 text-sm font-medium text-indigo-600 hover:text-indigo-500">
{product.reviewCount} reviews
</a>
</div>
</div>
</section>
<section aria-labelledby="options-heading" className="mt-10">
<h3 id="options-heading" className="sr-only">
Product options
</h3>
<form>
{/* Colors */}
<fieldset aria-label="Choose a color">
<legend className="text-sm font-medium text-gray-900">Color</legend>
<div className="mt-4 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>
{/* Sizes */}
<fieldset aria-label="Choose a size" className="mt-10">
<div className="flex items-center justify-between">
<div className="text-sm font-medium text-gray-900">Size</div>
<a href="#" className="text-sm font-medium text-indigo-600 hover:text-indigo-500">
Size guide
</a>
</div>
<div className="mt-2 grid grid-cols-4 gap-3">
{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>
<button
type="submit"
className="mt-6 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 bag
</button>
</form>
</section>
</div>
</div>
</div>
</DialogPanel>
</div>
</div>
</Dialog>
)
}