Black Weeks — Get lifetime access to all current themes 40% OFF on Full Access.
Use code LEX40 at checkout

Plus, new themes, lifetime updates, use on unlimited projects and lifetime support.

Limited time offer • Use code at checkout
Get full access

How to create a product details page with Tailwind CSS and Alpine.js

Published on November 10, 2025 by Michael Andreuzza

Building a Complete Ecommerce Product Page

Product detail pages are the heart of any ecommerce site. Today, we’ll create a comprehensive product page featuring an interactive image gallery, tabbed content sections, color and size selectors, customer reviews, and purchase actions—all built with Alpine.js and Tailwind CSS.

What You’ll Learn

  • How to create an interactive image gallery with thumbnails
  • Building tabbed content sections (Details, Shipping, Returns, Reviews)
  • Implementing color and size selection interfaces
  • Adding customer reviews with dynamic data
  • Creating responsive product layouts
  • Best practices for ecommerce UI components

The Complete Product Page Structure

Here’s the full product page code:

<Wrapper class="py-24">
  <div class="grid grid-cols-1 gap-8 lg:grid-cols-3">
    <!-- Left: Product copy + Tabs -->
    <div class="flex flex-col order-last lg:order-none">
      <div class="flex items-center justify-between">
        <h2 class="text-lg font-medium md:text-xl">
          Nike Air Force 1´07 Fresh
        </h2>
        <p class="text-lg md:text-xl">$190</p>
      </div>
      <p class="mt-4 text-base text-zinc-500">
        Hitting the field in the late '60s, adidas airmaxS quickly became
        soccer's "it" shoe.
      </p>
      <!-- Tabs -->
      <div
        x-data="{ tab: 'tab1' }"
        class="mt-8 divide-y divide-zinc-200 border-y border-zinc-200"
      >
        <ul
          class="flex gap-4 border-b border-zinc-200 text-sm text-zinc-500"
          role="tablist"
        >
          <li class="-mb-px">
            <a
              @click.prevent="tab = 'tab1'"
              href="#"
              role="tab"
              aria-selected="true"
              class="inline-block py-2 font-medium border-b-2 border-transparent"
              :class="{ 'text-zinc-700 border-zinc-600' : tab === 'tab1' }"
              >Details</a
            >
          </li>
          <li class="-mb-px">
            <a
              @click.prevent="tab = 'tab2'"
              href="#"
              role="tab"
              aria-selected="false"
              class="inline-block py-2 font-medium border-b-2 border-transparent"
              :class="{ 'text-zinc-700 border-zinc-600' : tab === 'tab2' }"
              >Shipping</a
            >
          </li>
          <li class="-mb-px">
            <a
              @click.prevent="tab = 'tab3'"
              href="#"
              role="tab"
              aria-selected="false"
              class="inline-block py-2 font-medium border-b-2 border-transparent"
              :class="{ 'text-zinc-700 border-zinc-600' : tab === 'tab3' }"
              >Returns</a
            >
          </li>
          <li class="-mb-px">
            <a
              @click.prevent="tab = 'tab4'"
              href="#"
              role="tab"
              aria-selected="false"
              class="inline-block py-2 font-medium border-b-2 border-transparent"
              :class="{ 'text-zinc-700 border-zinc-600' : tab === 'tab4' }"
              >Reviews</a
            >
          </li>
        </ul>

        <div class="content py-4 pt-4 text-left text-zinc-500">
          <div x-show="tab==='tab1'">
            <section class="py-4">
              <p class="text-sm">
                This product is crafted from high-quality materials designed for
                durability and comfort. It features modern design elements and
                is perfect for everyday use.
              </p>
              <ul
                role="list"
                class="mt-4 list-disc space-y-1 pl-5 text-xs text-zinc-600 marker:text-zinc-500"
              >
                <li>Versatile lace configurations</li>
                <li>Breathable and spacious interior fit</li>
                <li>Durable leather overlays</li>
                <li>Cushioned insole for added comfort</li>
                <li>Reinforced eyelets for secure lacing</li>
                <li>Stitching built for durability</li>
                <li>Water-resistant materials</li>
              </ul>
              <p class="mt-8 text-sm">
                <strong class="text-black">True to size</strong>. We recommend
                ordering your usual size.
              </p>
            </section>
          </div>

          <div x-show="tab==='tab2'" style="display: none">
            <section class="py-4">
              <p class="text-sm">
                We offer free standard shipping on all orders above $50. Express
                shipping options are available at checkout for an additional
                fee.
              </p>
            </section>
          </div>

          <div x-show="tab==='tab3'" style="display: none">
            <section class="py-4">
              <p class="text-sm">
                We accept returns within 30 days of purchase. Items must be in
                their original condition and packaging. Please visit our returns
                page for more details.
              </p>
            </section>
          </div>

          <div x-show="tab==='tab4'" style="display: none">
            <section class="py-4">
              <div
                class="flex flex-col"
                x-data="{ testimonials: [
                { name: 'Fernando Monte', date: '2025-12-01', datetime: '2025-12-01', img: '/avatars/1.png', text: 'The Nike Air Max 90 Black exceeded my expectations! Super comfortable and perfect for both running and casual wear.' },
                { name: 'Juan  Echanove', date: '2025-11-15', datetime: '2025-11-15', img: '/avatars/2.png', text: 'Stylish and lightweight. I wear them everywhere, but the fit was slightly snug at first. Broke in nicely after a few days.' }
              ] }"
              >
                <template x-for="t in testimonials" :key="t.datetime">
                  <div class="flex gap-x-4 py-4">
                    <div class="flex-none">
                      <img
                        :src="t.img"
                        :alt="t.name"
                        class="size-10 rounded-full bg-zinc-100"
                      />
                    </div>
                    <div>
                      <h3
                        class="text-base font-medium text-zinc-900"
                        x-text="t.name"
                      ></h3>
                      <p class="text-xs text-zinc-500">
                        <time :datetime="t.datetime" x-text="t.date"></time>
                      </p>
                      <p class="mt-1 text-sm text-zinc-500" x-text="t.text"></p>
                    </div>
                  </div>
                </template>
              </div>
              <div class="mt-8 flex">
                <button
                  class="relative flex h-9 w-full select-none items-center justify-center rounded-md bg-zinc-50 px-4 text-center text-sm font-medium text-zinc-600 outline outline-zinc-100 transition-colors duration-200 ease-in-out hover:bg-zinc-200 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
                >
                  Write a review
                </button>
              </div>
            </section>
          </div>
        </div>
      </div>
    </div>

    <!-- Middle: Product Image Gallery -->
    <div
      x-data="{
        activeImage: 0,
        images: [
          'https://oxbowui.com/store/shoes/airmax/1.png',
          'https://oxbowui.com/store/shoes/airmax/2.png',
          'https://oxbowui.com/store/shoes/airmax/3.png',
          'https://oxbowui.com/store/shoes/airmax/4.png',
          'https://oxbowui.com/store/shoes/airmax/5.png',
          'https://oxbowui.com/store/shoes/airmax/6.png'
        ]
      }"
      class="order-first flex flex-col gap-2 lg:order-none"
    >
      <!-- Main Image -->
      <div class="aspect-square overflow-hidden rounded-2xl bg-zinc-200">
        <img
          :src="images[activeImage]"
          class="size-full aspect-square object-cover"
          alt="Product image"
        />
      </div>
      <!-- Thumbnails -->
      <div class="grid w-full grid-cols-6 gap-2">
        <template x-for="(image, index) in images" :key="index">
          <button
            @click="activeImage = index"
            :class="{ 'ring-2 ring-zinc-900' : activeImage === index }"
            class="size-full aspect-square overflow-hidden rounded-xl bg-zinc-200"
            aria-label="Select image thumbnail"
          >
            <img
              :src="image"
              class="size-full aspect-square object-cover"
              alt="Product thumbnail"
            />
          </button>
        </template>
      </div>
    </div>

    <!-- Right: Product Options + Actions -->
    <div>
      <div class="mt-4 flex flex-col gap-4">
        <!-- Color Selector -->
        <div x-data="{ activeColor: null }">
          <p class="text-xs uppercase text-zinc-500">Color</p>
          <fieldset
            aria-label="Choose a color"
            class="mt-2"
            x-data="{
            colors: [
              { name: ' Black', ring: 'ring-zinc-700', bg: 'bg-zinc-400' },
              { name: ' Gray', ring: 'ring-zinc-300', bg: 'bg-zinc-200' },
              { name: ' Purple', ring: 'ring-purple-300', bg: 'bg-purple-200' },
              { name: ' Pink', ring: 'ring-pink-300', bg: 'bg-pink-200' },
              { name: ' Red', ring: 'ring-red-300', bg: 'bg-red-200' },
              { name: ' Orange', ring: 'ring-orange-300', bg: 'bg-orange-200' },
              { name: ' Yellow', ring: 'ring-yellow-300', bg: 'bg-yellow-200' },
              { name: ' Blue', ring: 'ring-blue-300', bg: 'bg-blue-200' },
              { name: ' Cyan', ring: 'ring-cyan-300', bg: 'ring-cyan-200' },
              { name: ' Green', ring: 'ring-green-300', bg: 'bg-green-200' },
              { name: ' Teal', ring: 'ring-teal-300', bg: 'bg-teal-200' },
            ],
            activeColor: null
          }"
          >
            <div class="flex flex-wrap items-center gap-3">
              <template x-for="color in colors" :key="color.name">
                <label
                  :aria-label="color.name"
                  class="relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 duration-300 focus:outline-none"
                  :class="{ 'ring-2 ring-offset-2 ring-zinc-900': activeColor === color.name }"
                >
                  <input
                    type="radio"
                    name="color-choice"
                    :value="color.name"
                    class="sr-only"
                    @click="activeColor = color.name"
                  />
                  <span
                    aria-hidden="true"
                    class="size-6 rounded-full ring-1"
                    :class="`${color.ring} ${color.bg}`"
                  ></span>
                </label>
              </template>
            </div>
          </fieldset>
        </div>

        <!-- Size Selector -->
        <div
          x-data="{ sizeSystem: 'US', activeSize: null, sizes: { US: ['6','7','8','9','10','11','12','13'], EU: ['39','40','41','42','43','44','45','46'] } }"
        >
          <div class="flex items-center justify-between">
            <p class="text-xs uppercase text-zinc-500">Shoe size</p>
            <div>
              <label for="size-system" class="sr-only">Size System</label>
              <select
                id="size-system"
                x-model="sizeSystem"
                @change="activeSize = null"
                class="block h-8 w-auto appearance-none rounded-lg border border-transparent bg-white pl-2.5 pr-8 py-2 text-xs text-zinc-900 ring-1 ring-zinc-200 placeholder-zinc-400 duration-300 focus:bg-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
              >
                <option value="US">US</option>
                <option value="EU">EU</option>
              </select>
            </div>
          </div>
          <div class="mt-2 grid grid-cols-4 gap-2 divide-x divide-zinc-200">
            <template x-for="size in sizes[sizeSystem]" :key="size">
              <div>
                <input
                  type="radio"
                  :id="'size-' + size"
                  :value="size"
                  name="size-choice"
                  x-model="activeSize"
                  class="peer sr-only"
                />
                <label
                  :for="'size-' + size"
                  x-text="size"
                  class="flex cursor-pointer items-center justify-center rounded-md bg-white px-3 py-2 text-sm font-medium text-zinc-500 ring-1 ring-zinc-200 duration-300 peer-checked:text-zinc-500 peer-checked:ring-2 peer-checked:ring-zinc-900 peer-checked:ring-offset-2"
                ></label>
              </div>
            </template>
          </div>
        </div>
      </div>

      <!-- Action Buttons -->
      <div class="mt-8 flex flex-col gap-2">
        <button
          class="relative flex h-9 w-full select-none items-center justify-center rounded-md bg-zinc-900 px-4 text-center text-sm font-medium text-white outline outline-zinc-900 transition-colors duration-200 ease-in-out hover:bg-zinc-950 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950"
        >
          Add to Cart
        </button>
        <button
          class="relative flex h-9 w-full select-none items-center justify-center rounded-md bg-zinc-50 px-4 text-center text-sm font-medium text-zinc-600 outline outline-zinc-100 transition-colors duration-200 ease-in-out hover:bg-zinc-200 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
        >
          Buy Now
        </button>
      </div>
      <p class="mt-1 text-sm text-zinc-500">
        Free shipping over $50 if you behave
      </p>
    </div>
  </div>
</Wrapper>

Breaking Down the Code

1. The Layout Grid

<div class="grid grid-cols-1 gap-8 lg:grid-cols-3"></div>
  • Responsive grid: Single column on mobile, three columns on large screens
  • Order classes: order-first and order-last control column positioning on mobile
  • Gap spacing: Consistent 2rem gap between sections

2. Product Information Section

<div class="flex items-center justify-between">
  <h2 class="text-lg font-medium md:text-xl">Nike Air Force 1´07 Fresh</h2>
  <p class="text-lg md:text-xl">$190</p>
</div>
  • Flexible header: Product name on left, price on right
  • Responsive typography: Larger text on medium+ screens

3. Tabbed Content System

<div
  x-data="{ tab: 'tab1' }"
  class="mt-8 divide-y divide-zinc-200 border-y border-zinc-200"
></div>
  • Alpine state: tab variable tracks active tab
  • Visual separators: Borders and dividers create clean sections
  • Tab navigation: Click handlers update the active tab state

4. Tab Content Areas

<div x-show="tab==='tab1'">
  <section class="py-4">
    <!-- Product details content -->
  </section>
</div>
  • Conditional display: x-show shows content based on active tab
  • Structured content: Each tab has semantic HTML sections
  • Product information: Details, shipping, returns, and reviews
<div
  x-data="{ activeImage: 0, images: [...] }"
  class="order-first flex flex-col gap-2 lg:order-none"
></div>
  • Gallery state: Tracks active image and image array
  • Main image display: Large aspect-square container
  • Thumbnail grid: 6-column grid of clickable thumbnails
  • Active state: Ring styling highlights selected thumbnail

6. Color Selection Interface

<fieldset
  aria-label="Choose a color"
  x-data="{ colors: [...], activeColor: null }"
></fieldset>
  • Accessible form: Proper fieldset and labels
  • Color array: Objects with names, ring colors, and backgrounds
  • Radio inputs: Screen reader accessible selection
  • Visual feedback: Ring styling for selected colors

7. Size Selection System

<div
  x-data="{ sizeSystem: 'US', activeSize: null, sizes: { US: [...], EU: [...] } }"
></div>
  • Dual sizing: US and EU size systems
  • Dynamic switching: Size system dropdown changes available options
  • Grid layout: 4-column responsive grid for size buttons
  • Peer styling: CSS peer classes for checked states

8. Action Buttons

<button
  class="relative flex h-9 w-full select-none items-center justify-center rounded-md bg-zinc-900 px-4 text-center text-sm font-medium text-white outline outline-zinc-900 transition-colors duration-200 ease-in-out hover:bg-zinc-950"
>
  Add to Cart
</button>
  • Primary CTA: Dark background, prominent styling
  • Secondary action: Light background, subtle styling
  • Accessibility: Focus states and proper contrast
  • Responsive: Full width on mobile, auto width on larger screens

Alpine.js State Management

The product page uses multiple Alpine.js data objects:

Tab Management

x-data="{ tab: 'tab1' }"
  • Simple string state for active tab
  • Click handlers update the tab value
x-data="{
  activeImage: 0,
  images: ['url1', 'url2', 'url3', ...]
}"
  • activeImage: Index of currently displayed image
  • images: Array of image URLs
  • Click handlers update activeImage

Color Selection

x-data="{ activeColor: null }"
  • Tracks selected color name
  • Radio button clicks update the value

Size Selection

x-data="{
  sizeSystem: 'US',
  activeSize: null,
  sizes: { US: [...], EU: [...] }
}"
  • sizeSystem: Current sizing system (US/EU)
  • activeSize: Selected size value
  • sizes: Object with size arrays for each system

Key Features Explained

Responsive Design: The layout adapts from single column on mobile to three columns on desktop, with proper content ordering.

Interactive Gallery: Click thumbnails to change the main product image, with visual feedback for the active selection.

Tabbed Information: Clean organization of product details, shipping, returns, and customer reviews.

Variant Selection: Color and size pickers with proper accessibility and visual feedback.

Customer Reviews: Dynamic testimonial display with user avatars and timestamps.

Call-to-Action: Prominent purchase buttons with hover states and accessibility features.

Customization Ideas

  • Add quantity selectors for multiple item purchases
  • Include product variant pricing (different prices for colors/sizes)
  • Add wishlist/favorite functionality
  • Implement zoom functionality for product images
  • Include product availability indicators
  • Add social sharing buttons
  • Integrate with shopping cart state management

This comprehensive product page provides an excellent user experience with smooth interactions, clear information hierarchy, and professional ecommerce functionality. The combination of Alpine.js reactivity and Tailwind CSS styling creates a modern, performant shopping experience.

Complete Code

<Wrapper class="py-24">
  <div class="grid grid-cols-1 gap-8 lg:grid-cols-3">
    <!-- Left: Product copy + Tabs -->
    <div class="flex flex-col order-last lg:order-none">
      <div class="flex items-center justify-between">
        <h2 class="text-lg font-medium md:text-xl">
          Nike Air Force 1´07 Fresh
        </h2>
        <p class="text-lg md:text-xl">$190</p>
      </div>
      <p class="mt-4 text-base text-zinc-500">
        Hitting the field in the late '60s, adidas airmaxS quickly became
        soccer's "it" shoe.
      </p>
      <!-- Tabs -->
      <div
        x-data="{ tab: 'tab1' }"
        class="mt-8 divide-y divide-zinc-200 border-y border-zinc-200"
      >
        <ul
          class="flex gap-4 border-b border-zinc-200 text-sm text-zinc-500"
          role="tablist"
        >
          <li class="-mb-px">
            <a
              @click.prevent="tab = 'tab1'"
              href="#"
              role="tab"
              aria-selected="true"
              class="inline-block py-2 font-medium border-b-2 border-transparent"
              :class="{ 'text-zinc-700 border-zinc-600' : tab === 'tab1' }"
              >Details</a
            >
          </li>
          <li class="-mb-px">
            <a
              @click.prevent="tab = 'tab2'"
              href="#"
              role="tab"
              aria-selected="false"
              class="inline-block py-2 font-medium border-b-2 border-transparent"
              :class="{ 'text-zinc-700 border-zinc-600' : tab === 'tab2' }"
              >Shipping</a
            >
          </li>
          <li class="-mb-px">
            <a
              @click.prevent="tab = 'tab3'"
              href="#"
              role="tab"
              aria-selected="false"
              class="inline-block py-2 font-medium border-b-2 border-transparent"
              :class="{ 'text-zinc-700 border-zinc-600' : tab === 'tab3' }"
              >Returns</a
            >
          </li>
          <li class="-mb-px">
            <a
              @click.prevent="tab = 'tab4'"
              href="#"
              role="tab"
              aria-selected="false"
              class="inline-block py-2 font-medium border-b-2 border-transparent"
              :class="{ 'text-zinc-700 border-zinc-600' : tab === 'tab4' }"
              >Reviews</a
            >
          </li>
        </ul>

        <div class="content py-4 pt-4 text-left text-zinc-500">
          <div x-show="tab==='tab1'">
            <section class="py-4">
              <p class="text-sm">
                This product is crafted from high-quality materials designed for
                durability and comfort. It features modern design elements and
                is perfect for everyday use.
              </p>
              <ul
                role="list"
                class="mt-4 list-disc space-y-1 pl-5 text-xs text-zinc-600 marker:text-zinc-500"
              >
                <li>Versatile lace configurations</li>
                <li>Breathable and spacious interior fit</li>
                <li>Durable leather overlays</li>
                <li>Cushioned insole for added comfort</li>
                <li>Reinforced eyelets for secure lacing</li>
                <li>Stitching built for durability</li>
                <li>Water-resistant materials</li>
              </ul>
              <p class="mt-8 text-sm">
                <strong class="text-black">True to size</strong>. We recommend
                ordering your usual size.
              </p>
            </section>
          </div>

          <div x-show="tab==='tab2'" style="display: none">
            <section class="py-4">
              <p class="text-sm">
                We offer free standard shipping on all orders above $50. Express
                shipping options are available at checkout for an additional
                fee.
              </p>
            </section>
          </div>

          <div x-show="tab==='tab3'" style="display: none">
            <section class="py-4">
              <p class="text-sm">
                We accept returns within 30 days of purchase. Items must be in
                their original condition and packaging. Please visit our returns
                page for more details.
              </p>
            </section>
          </div>

          <div x-show="tab==='tab4'" style="display: none">
            <section class="py-4">
              <div
                class="flex flex-col"
                x-data="{ testimonials: [
                { name: 'Fernando Monte', date: '2025-12-01', datetime: '2025-12-01', img: '/avatars/1.png', text: 'The Nike Air Max 90 Black exceeded my expectations! Super comfortable and perfect for both running and casual wear.' },
                { name: 'Juan  Echanove', date: '2025-11-15', datetime: '2025-11-15', img: '/avatars/2.png', text: 'Stylish and lightweight. I wear them everywhere, but the fit was slightly snug at first. Broke in nicely after a few days.' }
              ] }"
              >
                <template x-for="t in testimonials" :key="t.datetime">
                  <div class="flex gap-x-4 py-4">
                    <div class="flex-none">
                      <img
                        :src="t.img"
                        :alt="t.name"
                        class="size-10 rounded-full bg-zinc-100"
                      />
                    </div>
                    <div>
                      <h3
                        class="text-base font-medium text-zinc-900"
                        x-text="t.name"
                      ></h3>
                      <p class="text-xs text-zinc-500">
                        <time :datetime="t.datetime" x-text="t.date"></time>
                      </p>
                      <p class="mt-1 text-sm text-zinc-500" x-text="t.text"></p>
                    </div>
                  </div>
                </template>
              </div>
              <div class="mt-8 flex">
                <button
                  class="relative flex h-9 w-full select-none items-center justify-center rounded-md bg-zinc-50 px-4 text-center text-sm font-medium text-zinc-600 outline outline-zinc-100 transition-colors duration-200 ease-in-out hover:bg-zinc-200 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
                >
                  Write a review
                </button>
              </div>
            </section>
          </div>
        </div>
      </div>
    </div>

    <!-- Middle: Product Image Gallery -->
    <div
      x-data="{
        activeImage: 0,
        images: [
          'https://oxbowui.com/store/shoes/airmax/1.png',
          'https://oxbowui.com/store/shoes/airmax/2.png',
          'https://oxbowui.com/store/shoes/airmax/3.png',
          'https://oxbowui.com/store/shoes/airmax/4.png',
          'https://oxbowui.com/store/shoes/airmax/5.png',
          'https://oxbowui.com/store/shoes/airmax/6.png'
        ]
      }"
      class="order-first flex flex-col gap-2 lg:order-none"
    >
      <!-- Main Image -->
      <div class="aspect-square overflow-hidden rounded-2xl bg-zinc-200">
        <img
          :src="images[activeImage]"
          class="size-full aspect-square object-cover"
          alt="Product image"
        />
      </div>
      <!-- Thumbnails -->
      <div class="grid w-full grid-cols-6 gap-2">
        <template x-for="(image, index) in images" :key="index">
          <button
            @click="activeImage = index"
            :class="{ 'ring-2 ring-zinc-900' : activeImage === index }"
            class="size-full aspect-square overflow-hidden rounded-xl bg-zinc-200"
            aria-label="Select image thumbnail"
          >
            <img
              :src="image"
              class="size-full aspect-square object-cover"
              alt="Product thumbnail"
            />
          </button>
        </template>
      </div>
    </div>

    <!-- Right: Product Options + Actions -->
    <div>
      <div class="mt-4 flex flex-col gap-4">
        <!-- Color Selector -->
        <div x-data="{ activeColor: null }">
          <p class="text-xs uppercase text-zinc-500">Color</p>
          <fieldset
            aria-label="Choose a color"
            class="mt-2"
            x-data="{
            colors: [
              { name: ' Black', ring: 'ring-zinc-700', bg: 'bg-zinc-400' },
              { name: ' Gray', ring: 'ring-zinc-300', bg: 'bg-zinc-200' },
              { name: ' Purple', ring: 'ring-purple-300', bg: 'bg-purple-200' },
              { name: ' Pink', ring: 'ring-pink-300', bg: 'bg-pink-200' },
              { name: ' Red', ring: 'ring-red-300', bg: 'bg-red-200' },
              { name: ' Orange', ring: 'ring-orange-300', bg: 'bg-orange-200' },
              { name: ' Yellow', ring: 'ring-yellow-300', bg: 'bg-yellow-200' },
              { name: ' Blue', ring: 'ring-blue-300', bg: 'bg-blue-200' },
              { name: ' Cyan', ring: 'ring-cyan-300', bg: 'ring-cyan-200' },
              { name: ' Green', ring: 'ring-green-300', bg: 'bg-green-200' },
              { name: ' Teal', ring: 'ring-teal-300', bg: 'bg-teal-200' },
            ],
            activeColor: null
          }"
          >
            <div class="flex flex-wrap items-center gap-3">
              <template x-for="color in colors" :key="color.name">
                <label
                  :aria-label="color.name"
                  class="relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 duration-300 focus:outline-none"
                  :class="{ 'ring-2 ring-offset-2 ring-zinc-900': activeColor === color.name }"
                >
                  <input
                    type="radio"
                    name="color-choice"
                    :value="color.name"
                    class="sr-only"
                    @click="activeColor = color.name"
                  />
                  <span
                    aria-hidden="true"
                    class="size-6 rounded-full ring-1"
                    :class="`${color.ring} ${color.bg}`"
                  ></span>
                </label>
              </template>
            </div>
          </fieldset>
        </div>

        <!-- Size Selector -->
        <div
          x-data="{ sizeSystem: 'US', activeSize: null, sizes: { US: ['6','7','8','9','10','11','12','13'], EU: ['39','40','41','42','43','44','45','46'] } }"
        >
          <div class="flex items-center justify-between">
            <p class="text-xs uppercase text-zinc-500">Shoe size</p>
            <div>
              <label for="size-system" class="sr-only">Size System</label>
              <select
                id="size-system"
                x-model="sizeSystem"
                @change="activeSize = null"
                class="block h-8 w-auto appearance-none rounded-lg border border-transparent bg-white pl-2.5 pr-8 py-2 text-xs text-zinc-900 ring-1 ring-zinc-200 placeholder-zinc-400 duration-300 focus:bg-transparent focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
              >
                <option value="US">US</option>
                <option value="EU">EU</option>
              </select>
            </div>
          </div>
          <div class="mt-2 grid grid-cols-4 gap-2 divide-x divide-zinc-200">
            <template x-for="size in sizes[sizeSystem]" :key="size">
              <div>
                <input
                  type="radio"
                  :id="'size-' + size"
                  :value="size"
                  name="size-choice"
                  x-model="activeSize"
                  class="peer sr-only"
                />
                <label
                  :for="'size-' + size"
                  x-text="size"
                  class="flex cursor-pointer items-center justify-center rounded-md bg-white px-3 py-2 text-sm font-medium text-zinc-500 ring-1 ring-zinc-200 duration-300 peer-checked:text-zinc-500 peer-checked:ring-2 peer-checked:ring-zinc-900 peer-checked:ring-offset-2"
                ></label>
              </div>
            </template>
          </div>
        </div>
      </div>

      <!-- Action Buttons -->
      <div class="mt-8 flex flex-col gap-2">
        <button
          class="relative flex h-9 w-full select-none items-center justify-center rounded-md bg-zinc-900 px-4 text-center text-sm font-medium text-white outline outline-zinc-900 transition-colors duration-200 ease-in-out hover:bg-zinc-950 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950"
        >
          Add to Cart
        </button>
        <button
          class="relative flex h-9 w-full select-none items-center justify-center rounded-md bg-zinc-50 px-4 text-center text-sm font-medium text-zinc-600 outline outline-zinc-100 transition-colors duration-200 ease-in-out hover:bg-zinc-200 focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
        >
          Buy Now
        </button>
      </div>
      <p class="mt-1 text-sm text-zinc-500">
        Free shipping over $50 if you behave
      </p>
    </div>
  </div>
</Wrapper>

/Michael Andreuzza

Did you like this tutorial? Please share it with your friends!