01 / 22
All 22 in 22 Tailwind CSS Image Carousel
- 01Tailwind E-commerce Product Gallery Carousel with Thumbnails
- 02Tailwind Shop the Look Multi-Item Card Slider with Scroll Snap
- 03Tailwind Infinite Logo Scroller Marquee with No JavaScript
- 04Tailwind Product Colour Variant Picker Slider
- 05Tailwind Full-Screen Hero Slideshow with Auto Cross-Fade
- 06Tailwind Testimonial Card Carousel with Star Ratings
- 07Tailwind Meet the Team Profile Slider with Circular Portraits
- 08Tailwind Portfolio Gallery Slider with Hover Caption Reveal
- 09Tailwind Netflix-Style Content Row Carousel with Hover Expand
- 10Tailwind Magazine Split-Screen Article Slider
- 11Tailwind Onboarding Slider with Native CSS Scroll-Marker Dots
- 12Tailwind Vertical Image Carousel with Y-Axis Scroll Snap
- 13Tailwind 3D Coverflow Carousel with Perspective Transforms
- 14Tailwind Cross-Fade Image Slideshow with Manual Controls
- 15Tailwind Dual Opposite-Direction Marquee Slider
- 16Tailwind Dot Pagination Carousel with Target and Has Selectors
- 17Tailwind Prev / Next Arrow Carousel with Native Scroll Buttons
- 18Tailwind Before / After Image Comparison Slider
- 19Tailwind Accordion Expanding Image Panel Carousel
- 20Tailwind Autoplay Slider with Keyframe Track and Progress Bars
- 21Tailwind Zoom-on-Hover Image Gallery Slider
- 22Tailwind Parallax Text-Over-Image Story Slider
Tailwind E-commerce Product Gallery Carousel with Thumbnails
The product-page workhorse: one large image with a strip of clickable thumbnails underneath. The port's win is group-has-[#id:checked]: — twelve :nth-of-type sibling rules collapse into one utility per element, written on the element that actually changes.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcar-01 w-full min-h-svh grid place-items-center bg-tcar01-bg font-tcar01-sans px-5 py-10" aria-label="Product image gallery">
<div class="group w-[min(440px,100%)]">
<input type="radio" name="tcar-01" id="tcar-01-1" class="sr-only" checked>
<input type="radio" name="tcar-01" id="tcar-01-2" class="sr-only">
<input type="radio" name="tcar-01" id="tcar-01-3" class="sr-only">
<input type="radio" name="tcar-01" id="tcar-01-4" class="sr-only">
<div class="relative aspect-square rounded-2xl overflow-hidden bg-tcar01-ph shadow-[0_20px_45px_-30px_oklch(0.3_0.03_265/0.6)]">
<img src="https://picsum.photos/seed/sneaker-a/800/800" width="800" height="800" fetchpriority="high" decoding="async" alt="Trail runner, three-quarter view" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-1:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-b/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, sole detail" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-2:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-c/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, heel and laces" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-3:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-d/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, top-down view" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-4:checked]:opacity-100 motion-reduce:transition-none">
</div>
<div class="grid grid-cols-4 gap-2.5 mt-3.5">
<label for="tcar-01-1" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-1:checked]:border-tcar01-acc group-has-[#tcar-01-1:focus-visible]:outline-3 group-has-[#tcar-01-1:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-1:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-a/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-2" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-2:checked]:border-tcar01-acc group-has-[#tcar-01-2:focus-visible]:outline-3 group-has-[#tcar-01-2:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-2:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-b/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-3" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-3:checked]:border-tcar01-acc group-has-[#tcar-01-3:focus-visible]:outline-3 group-has-[#tcar-01-3:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-3:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-c/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-4" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-4:checked]:border-tcar01-acc group-has-[#tcar-01-4:focus-visible]:outline-3 group-has-[#tcar-01-4:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-4:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-d/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
</div>
</div>
</section><section class="tcar-01 w-full min-h-svh grid place-items-center bg-tcar01-bg font-tcar01-sans px-5 py-10" aria-label="Product image gallery">
<div class="group w-[min(440px,100%)]">
<input type="radio" name="tcar-01" id="tcar-01-1" class="sr-only" checked>
<input type="radio" name="tcar-01" id="tcar-01-2" class="sr-only">
<input type="radio" name="tcar-01" id="tcar-01-3" class="sr-only">
<input type="radio" name="tcar-01" id="tcar-01-4" class="sr-only">
<div class="relative aspect-square rounded-2xl overflow-hidden bg-tcar01-ph shadow-[0_20px_45px_-30px_oklch(0.3_0.03_265/0.6)]">
<img src="https://picsum.photos/seed/sneaker-a/800/800" width="800" height="800" fetchpriority="high" decoding="async" alt="Trail runner, three-quarter view" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-1:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-b/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, sole detail" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-2:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-c/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, heel and laces" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-3:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-d/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, top-down view" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-4:checked]:opacity-100 motion-reduce:transition-none">
</div>
<div class="grid grid-cols-4 gap-2.5 mt-3.5">
<label for="tcar-01-1" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-1:checked]:border-tcar01-acc group-has-[#tcar-01-1:focus-visible]:outline-3 group-has-[#tcar-01-1:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-1:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-a/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-2" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-2:checked]:border-tcar01-acc group-has-[#tcar-01-2:focus-visible]:outline-3 group-has-[#tcar-01-2:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-2:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-b/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-3" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-3:checked]:border-tcar01-acc group-has-[#tcar-01-3:focus-visible]:outline-3 group-has-[#tcar-01-3:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-3:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-c/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-4" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-4:checked]:border-tcar01-acc group-has-[#tcar-01-4:focus-visible]:outline-3 group-has-[#tcar-01-4:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-4:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-d/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-tcar01-bg: #f4f5f7;
--color-tcar01-bg: oklch(0.96 0.003 265);
--color-tcar01-ph: #e6e8ec;
--color-tcar01-ph: oklch(0.92 0.005 265);
--color-tcar01-acc: #3f7ad1;
--color-tcar01-acc: oklch(0.62 0.16 250);
--font-tcar01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the stage is sized before the browser build has compiled. */
.tcar-01 {
width: 100%;
min-height: 100svh;
}
/* ::selection takes no variant form. */
.tcar-01 ::selection {
background: var(--color-tcar01-acc);
color: #fff;
}@import "tailwindcss";
@theme {
--color-tcar01-bg: #f4f5f7;
--color-tcar01-bg: oklch(0.96 0.003 265);
--color-tcar01-ph: #e6e8ec;
--color-tcar01-ph: oklch(0.92 0.005 265);
--color-tcar01-acc: #3f7ad1;
--color-tcar01-acc: oklch(0.62 0.16 250);
--font-tcar01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the stage is sized before the browser build has compiled. */
.tcar-01 {
width: 100%;
min-height: 100svh;
}
/* ::selection takes no variant form. */
.tcar-01 ::selection {
background: var(--color-tcar01-acc);
color: #fff;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tcar01-bg: #f4f5f7;
--color-tcar01-bg: oklch(0.96 0.003 265);
--color-tcar01-ph: #e6e8ec;
--color-tcar01-ph: oklch(0.92 0.005 265);
--color-tcar01-acc: #3f7ad1;
--color-tcar01-acc: oklch(0.62 0.16 250);
--font-tcar01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the stage is sized before the browser build has compiled. */
.tcar-01 {
width: 100%;
min-height: 100svh;
}
/* ::selection takes no variant form. */
.tcar-01 ::selection {
background: var(--color-tcar01-acc);
color: #fff;
}@import "tailwindcss";
@theme {
--color-tcar01-bg: #f4f5f7;
--color-tcar01-bg: oklch(0.96 0.003 265);
--color-tcar01-ph: #e6e8ec;
--color-tcar01-ph: oklch(0.92 0.005 265);
--color-tcar01-acc: #3f7ad1;
--color-tcar01-acc: oklch(0.62 0.16 250);
--font-tcar01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the stage is sized before the browser build has compiled. */
.tcar-01 {
width: 100%;
min-height: 100svh;
}
/* ::selection takes no variant form. */
.tcar-01 ::selection {
background: var(--color-tcar01-acc);
color: #fff;
}<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;800&display=swap">
<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;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The twelve
:nth-of-type(N):checked ~ .stage .main:nth-child(N)selectors become onegroup-has-[#tcar-01-N:checked]:opacity-100per image. The rule count drops from twelve to four, and each one sits on the element it governs. peer-checked:looks like the obvious port and is a trap: it emits a sibling combinator, and the stage images are not siblings of the radios.group-has-is the only variant that crosses the nesting boundary.- UI change from the CSS original: images below the first now carry
loading="lazy" decoding="async"and the first carriesfetchpriority="high"— the LCP candidate is fetched first and the other three stop competing with it. The CSS version eagerly loaded all eight files.
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 E-commerce Product Gallery Carousel with Thumbnails
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-ecommerce-product-gallery-carousel-thumbnails/
The product-page workhorse: one large image with a strip of clickable thumbnails underneath. The port's win is group-has-[#id:checked]: — twelve :nth-of-type sibling rules collapse into one utility per element, written on the element that actually changes.
## HTML
```html
<section class="tcar-01 w-full min-h-svh grid place-items-center bg-tcar01-bg font-tcar01-sans px-5 py-10" aria-label="Product image gallery">
<div class="group w-[min(440px,100%)]">
<input type="radio" name="tcar-01" id="tcar-01-1" class="sr-only" checked>
<input type="radio" name="tcar-01" id="tcar-01-2" class="sr-only">
<input type="radio" name="tcar-01" id="tcar-01-3" class="sr-only">
<input type="radio" name="tcar-01" id="tcar-01-4" class="sr-only">
<div class="relative aspect-square rounded-2xl overflow-hidden bg-tcar01-ph shadow-[0_20px_45px_-30px_oklch(0.3_0.03_265/0.6)]">
<img src="https://picsum.photos/seed/sneaker-a/800/800" width="800" height="800" fetchpriority="high" decoding="async" alt="Trail runner, three-quarter view" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-1:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-b/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, sole detail" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-2:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-c/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, heel and laces" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-3:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-d/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, top-down view" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-4:checked]:opacity-100 motion-reduce:transition-none">
</div>
<div class="grid grid-cols-4 gap-2.5 mt-3.5">
<label for="tcar-01-1" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-1:checked]:border-tcar01-acc group-has-[#tcar-01-1:focus-visible]:outline-3 group-has-[#tcar-01-1:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-1:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-a/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-2" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-2:checked]:border-tcar01-acc group-has-[#tcar-01-2:focus-visible]:outline-3 group-has-[#tcar-01-2:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-2:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-b/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-3" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-3:checked]:border-tcar01-acc group-has-[#tcar-01-3:focus-visible]:outline-3 group-has-[#tcar-01-3:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-3:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-c/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-4" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-4:checked]:border-tcar01-acc group-has-[#tcar-01-4:focus-visible]:outline-3 group-has-[#tcar-01-4:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-4:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-d/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcar01-bg: #f4f5f7;
--color-tcar01-bg: oklch(0.96 0.003 265);
--color-tcar01-ph: #e6e8ec;
--color-tcar01-ph: oklch(0.92 0.005 265);
--color-tcar01-acc: #3f7ad1;
--color-tcar01-acc: oklch(0.62 0.16 250);
--font-tcar01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the stage is sized before the browser build has compiled. */
.tcar-01 {
width: 100%;
min-height: 100svh;
}
/* ::selection takes no variant form. */
.tcar-01 ::selection {
background: var(--color-tcar01-acc);
color: #fff;
}
```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 E-commerce Product Gallery Carousel with Thumbnails
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-ecommerce-product-gallery-carousel-thumbnails/
The product-page workhorse: one large image with a strip of clickable thumbnails underneath. The port's win is group-has-[#id:checked]: — twelve :nth-of-type sibling rules collapse into one utility per element, written on the element that actually changes.
## HTML
```html
<section class="tcar-01 w-full min-h-svh grid place-items-center bg-tcar01-bg font-tcar01-sans px-5 py-10" aria-label="Product image gallery">
<div class="group w-[min(440px,100%)]">
<input type="radio" name="tcar-01" id="tcar-01-1" class="sr-only" checked>
<input type="radio" name="tcar-01" id="tcar-01-2" class="sr-only">
<input type="radio" name="tcar-01" id="tcar-01-3" class="sr-only">
<input type="radio" name="tcar-01" id="tcar-01-4" class="sr-only">
<div class="relative aspect-square rounded-2xl overflow-hidden bg-tcar01-ph shadow-[0_20px_45px_-30px_oklch(0.3_0.03_265/0.6)]">
<img src="https://picsum.photos/seed/sneaker-a/800/800" width="800" height="800" fetchpriority="high" decoding="async" alt="Trail runner, three-quarter view" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-1:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-b/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, sole detail" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-2:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-c/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, heel and laces" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-3:checked]:opacity-100 motion-reduce:transition-none">
<img src="https://picsum.photos/seed/sneaker-d/800/800" width="800" height="800" loading="lazy" decoding="async" alt="Trail runner, top-down view" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[450ms] ease-out group-has-[#tcar-01-4:checked]:opacity-100 motion-reduce:transition-none">
</div>
<div class="grid grid-cols-4 gap-2.5 mt-3.5">
<label for="tcar-01-1" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-1:checked]:border-tcar01-acc group-has-[#tcar-01-1:focus-visible]:outline-3 group-has-[#tcar-01-1:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-1:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-a/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-2" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-2:checked]:border-tcar01-acc group-has-[#tcar-01-2:focus-visible]:outline-3 group-has-[#tcar-01-2:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-2:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-b/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-3" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-3:checked]:border-tcar01-acc group-has-[#tcar-01-3:focus-visible]:outline-3 group-has-[#tcar-01-3:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-3:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-c/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
<label for="tcar-01-4" class="block aspect-square rounded-xl overflow-hidden cursor-pointer bg-tcar01-ph border-2 border-transparent transition-[border-color,translate] duration-200 hover:-translate-y-0.5 group-has-[#tcar-01-4:checked]:border-tcar01-acc group-has-[#tcar-01-4:focus-visible]:outline-3 group-has-[#tcar-01-4:focus-visible]:outline-tcar01-acc group-has-[#tcar-01-4:focus-visible]:outline-offset-2 motion-reduce:transition-colors motion-reduce:hover:translate-y-0"><img src="https://picsum.photos/seed/sneaker-d/160/160" width="160" height="160" loading="lazy" decoding="async" alt="" aria-hidden="true" class="size-full object-cover block"></label>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcar01-bg: #f4f5f7;
--color-tcar01-bg: oklch(0.96 0.003 265);
--color-tcar01-ph: #e6e8ec;
--color-tcar01-ph: oklch(0.92 0.005 265);
--color-tcar01-acc: #3f7ad1;
--color-tcar01-acc: oklch(0.62 0.16 250);
--font-tcar01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the stage is sized before the browser build has compiled. */
.tcar-01 {
width: 100%;
min-height: 100svh;
}
/* ::selection takes no variant form. */
.tcar-01 ::selection {
background: var(--color-tcar01-acc);
color: #fff;
}
```How this works
The sibling map becomes a :has() condition. The CSS version needs one selector per (radio × target) pair, twelve in total. Tailwind puts group on the viewer and the condition on each element:
<img class="opacity-0 group-has-[#tcar-01-2:checked]:opacity-100">Depth stops mattering, so the identical variant lights the thumbnail ring two levels away, and adding a fifth view is additive rather than a renumbering exercise.
The radios are still radios. sr-only clips them to a 1px box but leaves them focusable and in the accessibility tree, so Left/Right steps the gallery natively and the group announces "1 of 4".
Focus is mirrored, not removed. group-has-[#tcar-01-2:focus-visible]:outline-3 paints the ring on the thumbnail the keyboard is on, so mouse and keyboard states stay in lockstep without a single outline:none.
Make it yours
- Add a view by adding one radio, one stage image and one thumb, each carrying its own
group-has-[#tcar-01-5:checked]:utility — no existing class changes. - Retheme through
--color-tcar01-acc: active ring and focus ring both read it. - Instant cut instead of a dissolve: drop
transition-opacity duration-[450ms]from the stage images. - Thumbs per row: change
grid-cols-4.
Gotchas — read before shipping
peer-checked:is the wrong tool here — it compiles to.peer:checked ~ &, a sibling combinator that cannot reach the stage image inside a sibling div. It fails silently. Usegroup-has-[...].- Never swap
sr-onlyforhiddenon the radios:display:noneremoves them from the tab order and the accessibility tree, and arrow-key navigation dies with it. - Keep
aspect-squareon the stage. Without a reserved box the image swap shifts the page and CLS spikes.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() drives the whole state map (Chrome 105+, Safari 15.4+, Firefox 121+), which sits inside Tailwind v4's own floor (cascade layers, @property, color-mix()).