Pure TailwindMIT licensed

Tailwind Product Colour Variant Picker Slider

Click a swatch to swap the product photo — the apparel and furniture variant picker. Three elements change per choice (photo, swatch ring, colour caption) and in Tailwind all three carry the same group-has-[#id:checked]: condition, which is the clearest argument for the pattern.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tcar-04 w-full min-h-svh grid place-items-center bg-tcar04-bg font-tcar04-sans px-5 py-10" aria-label="Product colour variant picker">
  <div class="group w-[min(400px,100%)] bg-white rounded-[18px] overflow-hidden shadow-[0_24px_50px_-34px_oklch(0.3_0.03_265/0.6)]">
    <input type="radio" name="tcar-04" id="tcar-04-1" class="sr-only" checked>
    <input type="radio" name="tcar-04" id="tcar-04-2" class="sr-only">
    <input type="radio" name="tcar-04" id="tcar-04-3" class="sr-only">
    <div class="relative aspect-square bg-tcar04-ph">
      <img src="https://picsum.photos/seed/chair-slate/700/700" width="700" height="700" fetchpriority="high" decoding="async" alt="Lounge chair in slate" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-500 ease-out group-has-[#tcar-04-1:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/chair-ochre/700/700" width="700" height="700" loading="lazy" decoding="async" alt="Lounge chair in ochre" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-500 ease-out group-has-[#tcar-04-2:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/chair-moss/700/700" width="700" height="700" loading="lazy" decoding="async" alt="Lounge chair in moss" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-500 ease-out group-has-[#tcar-04-3:checked]:opacity-100 motion-reduce:transition-none">
    </div>
    <div class="flex items-center justify-between gap-3.5 px-5 pt-4 pb-5">
      <p class="text-sm text-tcar04-mute m-0"><span class="font-semibold text-tcar04-ink">Colour:</span>
        <span class="hidden font-semibold text-tcar04-ink group-has-[#tcar-04-1:checked]:inline">Slate</span><span class="hidden font-semibold text-tcar04-ink group-has-[#tcar-04-2:checked]:inline">Ochre</span><span class="hidden font-semibold text-tcar04-ink group-has-[#tcar-04-3:checked]:inline">Moss</span>
      </p>
      <div class="flex gap-4">
        <label for="tcar-04-1" aria-label="Slate" style="background:#5b6470" class="relative size-7 rounded-full cursor-pointer border-2 border-white shadow-[0_0_0_1px_oklch(0_0_0/0.14)] transition-[scale,box-shadow] duration-200 before:content-[''] before:absolute before:-inset-2 before:rounded-full hover:scale-[1.08] group-has-[#tcar-04-1:checked]:scale-[1.12] group-has-[#tcar-04-1:checked]:shadow-[0_0_0_2px_var(--color-tcar04-acc)] group-has-[#tcar-04-1:focus-visible]:outline-3 group-has-[#tcar-04-1:focus-visible]:outline-tcar04-acc group-has-[#tcar-04-1:focus-visible]:outline-offset-[3px] motion-reduce:transition-none"></label>
        <label for="tcar-04-2" aria-label="Ochre" style="background:#c98a2b" class="relative size-7 rounded-full cursor-pointer border-2 border-white shadow-[0_0_0_1px_oklch(0_0_0/0.14)] transition-[scale,box-shadow] duration-200 before:content-[''] before:absolute before:-inset-2 before:rounded-full hover:scale-[1.08] group-has-[#tcar-04-2:checked]:scale-[1.12] group-has-[#tcar-04-2:checked]:shadow-[0_0_0_2px_var(--color-tcar04-acc)] group-has-[#tcar-04-2:focus-visible]:outline-3 group-has-[#tcar-04-2:focus-visible]:outline-tcar04-acc group-has-[#tcar-04-2:focus-visible]:outline-offset-[3px] motion-reduce:transition-none"></label>
        <label for="tcar-04-3" aria-label="Moss" style="background:#6f8256" class="relative size-7 rounded-full cursor-pointer border-2 border-white shadow-[0_0_0_1px_oklch(0_0_0/0.14)] transition-[scale,box-shadow] duration-200 before:content-[''] before:absolute before:-inset-2 before:rounded-full hover:scale-[1.08] group-has-[#tcar-04-3:checked]:scale-[1.12] group-has-[#tcar-04-3:checked]:shadow-[0_0_0_2px_var(--color-tcar04-acc)] group-has-[#tcar-04-3:focus-visible]:outline-3 group-has-[#tcar-04-3:focus-visible]:outline-tcar04-acc group-has-[#tcar-04-3:focus-visible]:outline-offset-[3px] motion-reduce:transition-none"></label>
      </div>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tcar04-bg: #f2f0ec;
  --color-tcar04-bg: oklch(0.95 0.006 85);
  --color-tcar04-ph: #e9e6e1;
  --color-tcar04-ph: oklch(0.93 0.006 85);
  --color-tcar04-ink: #20242c;
  --color-tcar04-ink: oklch(0.26 0.012 265);
  --color-tcar04-mute: #5a5f68;
  --color-tcar04-mute: oklch(0.48 0.012 265);
  --color-tcar04-acc: #1f2430;
  --color-tcar04-acc: oklch(0.27 0.02 265);
  --font-tcar04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tcar-04 {
  width: 100%;
  min-height: 100svh;
}

.tcar-04 ::selection {
  background: var(--color-tcar04-acc);
  color: #fff;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tcar04-bg: #f2f0ec;
  --color-tcar04-bg: oklch(0.95 0.006 85);
  --color-tcar04-ph: #e9e6e1;
  --color-tcar04-ph: oklch(0.93 0.006 85);
  --color-tcar04-ink: #20242c;
  --color-tcar04-ink: oklch(0.26 0.012 265);
  --color-tcar04-mute: #5a5f68;
  --color-tcar04-mute: oklch(0.48 0.012 265);
  --color-tcar04-acc: #1f2430;
  --color-tcar04-acc: oklch(0.27 0.02 265);
  --font-tcar04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tcar-04 {
  width: 100%;
  min-height: 100svh;
}

.tcar-04 ::selection {
  background: var(--color-tcar04-acc);
  color: #fff;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Nine :checked ~ selectors become three group-has-[...] conditions, each written once and reused verbatim at three different depths. This demo is the clearest evidence for the pattern in the collection.
  • Swatch colours stay as inline style rather than @theme entries: a product colourway is content, not a design token, and minting --color-tcar04-ochre would imply the theme owns it.
  • UI change from the CSS original: each 28px swatch gained a before:-inset-2 invisible hit area, taking the touch target to 44×44px and satisfying WCAG 2.5.5 — the original's swatches were 28px of tappable area on a phone.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Product Colour Variant Picker Slider
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-product-colour-variant-picker-slider/

Click a swatch to swap the product photo — the apparel and furniture variant picker. Three elements change per choice (photo, swatch ring, colour caption) and in Tailwind all three carry the same group-has-[#id:checked]: condition, which is the clearest argument for the pattern.
## HTML
```html
<section class="tcar-04 w-full min-h-svh grid place-items-center bg-tcar04-bg font-tcar04-sans px-5 py-10" aria-label="Product colour variant picker">
  <div class="group w-[min(400px,100%)] bg-white rounded-[18px] overflow-hidden shadow-[0_24px_50px_-34px_oklch(0.3_0.03_265/0.6)]">
    <input type="radio" name="tcar-04" id="tcar-04-1" class="sr-only" checked>
    <input type="radio" name="tcar-04" id="tcar-04-2" class="sr-only">
    <input type="radio" name="tcar-04" id="tcar-04-3" class="sr-only">
    <div class="relative aspect-square bg-tcar04-ph">
      <img src="https://picsum.photos/seed/chair-slate/700/700" width="700" height="700" fetchpriority="high" decoding="async" alt="Lounge chair in slate" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-500 ease-out group-has-[#tcar-04-1:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/chair-ochre/700/700" width="700" height="700" loading="lazy" decoding="async" alt="Lounge chair in ochre" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-500 ease-out group-has-[#tcar-04-2:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/chair-moss/700/700" width="700" height="700" loading="lazy" decoding="async" alt="Lounge chair in moss" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-500 ease-out group-has-[#tcar-04-3:checked]:opacity-100 motion-reduce:transition-none">
    </div>
    <div class="flex items-center justify-between gap-3.5 px-5 pt-4 pb-5">
      <p class="text-sm text-tcar04-mute m-0"><span class="font-semibold text-tcar04-ink">Colour:</span>
        <span class="hidden font-semibold text-tcar04-ink group-has-[#tcar-04-1:checked]:inline">Slate</span><span class="hidden font-semibold text-tcar04-ink group-has-[#tcar-04-2:checked]:inline">Ochre</span><span class="hidden font-semibold text-tcar04-ink group-has-[#tcar-04-3:checked]:inline">Moss</span>
      </p>
      <div class="flex gap-4">
        <label for="tcar-04-1" aria-label="Slate" style="background:#5b6470" class="relative size-7 rounded-full cursor-pointer border-2 border-white shadow-[0_0_0_1px_oklch(0_0_0/0.14)] transition-[scale,box-shadow] duration-200 before:content-[''] before:absolute before:-inset-2 before:rounded-full hover:scale-[1.08] group-has-[#tcar-04-1:checked]:scale-[1.12] group-has-[#tcar-04-1:checked]:shadow-[0_0_0_2px_var(--color-tcar04-acc)] group-has-[#tcar-04-1:focus-visible]:outline-3 group-has-[#tcar-04-1:focus-visible]:outline-tcar04-acc group-has-[#tcar-04-1:focus-visible]:outline-offset-[3px] motion-reduce:transition-none"></label>
        <label for="tcar-04-2" aria-label="Ochre" style="background:#c98a2b" class="relative size-7 rounded-full cursor-pointer border-2 border-white shadow-[0_0_0_1px_oklch(0_0_0/0.14)] transition-[scale,box-shadow] duration-200 before:content-[''] before:absolute before:-inset-2 before:rounded-full hover:scale-[1.08] group-has-[#tcar-04-2:checked]:scale-[1.12] group-has-[#tcar-04-2:checked]:shadow-[0_0_0_2px_var(--color-tcar04-acc)] group-has-[#tcar-04-2:focus-visible]:outline-3 group-has-[#tcar-04-2:focus-visible]:outline-tcar04-acc group-has-[#tcar-04-2:focus-visible]:outline-offset-[3px] motion-reduce:transition-none"></label>
        <label for="tcar-04-3" aria-label="Moss" style="background:#6f8256" class="relative size-7 rounded-full cursor-pointer border-2 border-white shadow-[0_0_0_1px_oklch(0_0_0/0.14)] transition-[scale,box-shadow] duration-200 before:content-[''] before:absolute before:-inset-2 before:rounded-full hover:scale-[1.08] group-has-[#tcar-04-3:checked]:scale-[1.12] group-has-[#tcar-04-3:checked]:shadow-[0_0_0_2px_var(--color-tcar04-acc)] group-has-[#tcar-04-3:focus-visible]:outline-3 group-has-[#tcar-04-3:focus-visible]:outline-tcar04-acc group-has-[#tcar-04-3:focus-visible]:outline-offset-[3px] motion-reduce:transition-none"></label>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcar04-bg: #f2f0ec;
  --color-tcar04-bg: oklch(0.95 0.006 85);
  --color-tcar04-ph: #e9e6e1;
  --color-tcar04-ph: oklch(0.93 0.006 85);
  --color-tcar04-ink: #20242c;
  --color-tcar04-ink: oklch(0.26 0.012 265);
  --color-tcar04-mute: #5a5f68;
  --color-tcar04-mute: oklch(0.48 0.012 265);
  --color-tcar04-acc: #1f2430;
  --color-tcar04-acc: oklch(0.27 0.02 265);
  --font-tcar04-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tcar-04 {
  width: 100%;
  min-height: 100svh;
}

.tcar-04 ::selection {
  background: var(--color-tcar04-acc);
  color: #fff;
}
```

How this works

One condition, every layer. Choosing ochre must change three things at three nesting depths. The CSS version writes nine selectors; Tailwind writes group-has-[#tcar-04-2:checked]: three times, each on the element it affects.

The caption is the accessibility argument. Each colour name is a <span class="hidden group-has-[#tcar-04-2:checked]:inline">, so the selected variant is stated in words. A colour-blind user reads "Colour: Ochre" rather than inferring it from a ring.

The swatch grew a hit area. A 28px circle is a fine visual and a poor touch target; before:-inset-2 extends the tappable box to 44px without moving a pixel.

Make it yours

  • Add a variant: one radio, one stage image, one swatch, one caption word, each with its own group-has-[#tcar-04-4:checked]:.
  • Real colours stay inline style="background:#c98a2b" — product colours are data, not theme tokens.
  • Fabric thumbnails instead of flat colour: drop an <img> inside the label.
  • Ring colour: --color-tcar04-acc.

Gotchas — read before shipping

  • Every swatch label needs an aria-label, or its accessible name is the empty string and the radio is unusable by voice control.
  • Keep the caption. A ring alone fails WCAG 1.4.1 (Use of Colour) for the one group of users most affected by a colour picker.
  • aspect-square on the stage, always — variant photos swap on click and an unreserved box shifts the whole page.
  • Whenever you inflate a control with before:-inset-*, check the gap: the hit area must be no wider than the pitch (visible size + gap), or adjacent targets overlap and one swatch steals its neighbour's clicks.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:has() drives the state map (Chrome 105+, Safari 15.4+, Firefox 121+); the floor quoted is Tailwind v4's generated sheet.

Techniques used in this demo

HTML & JSno Tailwind involved

Search CodeFronts

Loading…