16 / 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 Dot Pagination Carousel with Target and Has Selectors
A wide-format gallery with indicator dots that jump to their slide and light up when active. The dots are anchor links, the highlight is :target read through :has(), and the whole thing is navigation that works with JavaScript disabled.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcar-16 w-full min-h-svh grid place-items-center bg-tcar16-bg font-tcar16-sans px-5 py-10" aria-label="Destination gallery">
<div class="group tcar16-frame w-[min(680px,100%)] bg-white rounded-[18px] overflow-hidden shadow-[0_24px_50px_-34px_oklch(0.45_0.1_75/0.5)]">
<ul tabindex="0" role="group" aria-label="Destinations, swipe or use arrow keys" class="tcar16-scroller list-none flex overflow-x-auto m-0 p-0 snap-x snap-mandatory scroll-smooth [scrollbar-width:none] focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:-outline-offset-[3px]">
<li id="tcar-16-1" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-1/1000/560" width="1000" height="560" fetchpriority="high" decoding="async" alt="Amalfi coastline" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Amalfi Coast</span></li>
<li id="tcar-16-2" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-2/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Santorini rooftops" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Santorini</span></li>
<li id="tcar-16-3" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-3/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Kyoto temple garden" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Kyoto</span></li>
<li id="tcar-16-4" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-4/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Patagonia peaks" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Patagonia</span></li>
</ul>
<nav aria-label="Pagination" class="flex justify-center p-1 bg-white">
<a href="#tcar-16-1" aria-label="Go to slide 1" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-1:target]:bg-tcar16-acc group-has-[#tcar-16-1:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-2" aria-label="Go to slide 2" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-2:target]:bg-tcar16-acc group-has-[#tcar-16-2:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-3" aria-label="Go to slide 3" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-3:target]:bg-tcar16-acc group-has-[#tcar-16-3:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-4" aria-label="Go to slide 4" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-4:target]:bg-tcar16-acc group-has-[#tcar-16-4:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
</nav>
</div>
</section><section class="tcar-16 w-full min-h-svh grid place-items-center bg-tcar16-bg font-tcar16-sans px-5 py-10" aria-label="Destination gallery">
<div class="group tcar16-frame w-[min(680px,100%)] bg-white rounded-[18px] overflow-hidden shadow-[0_24px_50px_-34px_oklch(0.45_0.1_75/0.5)]">
<ul tabindex="0" role="group" aria-label="Destinations, swipe or use arrow keys" class="tcar16-scroller list-none flex overflow-x-auto m-0 p-0 snap-x snap-mandatory scroll-smooth [scrollbar-width:none] focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:-outline-offset-[3px]">
<li id="tcar-16-1" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-1/1000/560" width="1000" height="560" fetchpriority="high" decoding="async" alt="Amalfi coastline" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Amalfi Coast</span></li>
<li id="tcar-16-2" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-2/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Santorini rooftops" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Santorini</span></li>
<li id="tcar-16-3" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-3/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Kyoto temple garden" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Kyoto</span></li>
<li id="tcar-16-4" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-4/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Patagonia peaks" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Patagonia</span></li>
</ul>
<nav aria-label="Pagination" class="flex justify-center p-1 bg-white">
<a href="#tcar-16-1" aria-label="Go to slide 1" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-1:target]:bg-tcar16-acc group-has-[#tcar-16-1:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-2" aria-label="Go to slide 2" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-2:target]:bg-tcar16-acc group-has-[#tcar-16-2:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-3" aria-label="Go to slide 3" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-3:target]:bg-tcar16-acc group-has-[#tcar-16-3:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-4" aria-label="Go to slide 4" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-4:target]:bg-tcar16-acc group-has-[#tcar-16-4:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
</nav>
</div>
</section>@import "tailwindcss";
@theme {
--color-tcar16-bg: #faf6ee;
--color-tcar16-bg: oklch(0.97 0.012 85);
--color-tcar16-dot: #dcc9a4;
--color-tcar16-dot: oklch(0.85 0.045 85);
--color-tcar16-dothover: #c4a873;
--color-tcar16-dothover: oklch(0.75 0.07 85);
--color-tcar16-acc: #b26a00;
--color-tcar16-acc: oklch(0.6 0.13 70);
--font-tcar16-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-16 {
width: 100%;
min-height: 100svh;
}
.tcar-16 ::selection {
background: var(--color-tcar16-acc);
color: #fff;
}
.tcar16-scroller::-webkit-scrollbar {
display: none;
}
/* First paint: nothing is :target yet, so slide 1 owns the highlight.
:not(:has(:target)) has no readable variant form — this is the rule that stays CSS. */
.tcar16-frame:not(:has(:target)) .tcar16-dot:first-child > span {
background: var(--color-tcar16-acc);
scale: 1.4;
}
@media (prefers-reduced-motion: reduce) {
.tcar16-scroller {
scroll-behavior: auto;
}
}@import "tailwindcss";
@theme {
--color-tcar16-bg: #faf6ee;
--color-tcar16-bg: oklch(0.97 0.012 85);
--color-tcar16-dot: #dcc9a4;
--color-tcar16-dot: oklch(0.85 0.045 85);
--color-tcar16-dothover: #c4a873;
--color-tcar16-dothover: oklch(0.75 0.07 85);
--color-tcar16-acc: #b26a00;
--color-tcar16-acc: oklch(0.6 0.13 70);
--font-tcar16-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-16 {
width: 100%;
min-height: 100svh;
}
.tcar-16 ::selection {
background: var(--color-tcar16-acc);
color: #fff;
}
.tcar16-scroller::-webkit-scrollbar {
display: none;
}
/* First paint: nothing is :target yet, so slide 1 owns the highlight.
:not(:has(:target)) has no readable variant form — this is the rule that stays CSS. */
.tcar16-frame:not(:has(:target)) .tcar16-dot:first-child > span {
background: var(--color-tcar16-acc);
scale: 1.4;
}
@media (prefers-reduced-motion: reduce) {
.tcar16-scroller {
scroll-behavior: auto;
}
}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-tcar16-bg: #faf6ee;
--color-tcar16-bg: oklch(0.97 0.012 85);
--color-tcar16-dot: #dcc9a4;
--color-tcar16-dot: oklch(0.85 0.045 85);
--color-tcar16-dothover: #c4a873;
--color-tcar16-dothover: oklch(0.75 0.07 85);
--color-tcar16-acc: #b26a00;
--color-tcar16-acc: oklch(0.6 0.13 70);
--font-tcar16-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-16 {
width: 100%;
min-height: 100svh;
}
.tcar-16 ::selection {
background: var(--color-tcar16-acc);
color: #fff;
}
.tcar16-scroller::-webkit-scrollbar {
display: none;
}
/* First paint: nothing is :target yet, so slide 1 owns the highlight.
:not(:has(:target)) has no readable variant form — this is the rule that stays CSS. */
.tcar16-frame:not(:has(:target)) .tcar16-dot:first-child > span {
background: var(--color-tcar16-acc);
scale: 1.4;
}
@media (prefers-reduced-motion: reduce) {
.tcar16-scroller {
scroll-behavior: auto;
}
}@import "tailwindcss";
@theme {
--color-tcar16-bg: #faf6ee;
--color-tcar16-bg: oklch(0.97 0.012 85);
--color-tcar16-dot: #dcc9a4;
--color-tcar16-dot: oklch(0.85 0.045 85);
--color-tcar16-dothover: #c4a873;
--color-tcar16-dothover: oklch(0.75 0.07 85);
--color-tcar16-acc: #b26a00;
--color-tcar16-acc: oklch(0.6 0.13 70);
--font-tcar16-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-16 {
width: 100%;
min-height: 100svh;
}
.tcar-16 ::selection {
background: var(--color-tcar16-acc);
color: #fff;
}
.tcar16-scroller::-webkit-scrollbar {
display: none;
}
/* First paint: nothing is :target yet, so slide 1 owns the highlight.
:not(:has(:target)) has no readable variant form — this is the rule that stays CSS. */
.tcar16-frame:not(:has(:target)) .tcar16-dot:first-child > span {
background: var(--color-tcar16-acc);
scale: 1.4;
}
@media (prefers-reduced-motion: reduce) {
.tcar16-scroller {
scroll-behavior: auto;
}
}<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">
<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.
group-has-[#tcar-16-2:target]:is the same variant the radio demos use, pointed at a different pseudo-class. Once the:has()pattern is in hand,:checked,:targetand:focus-visibleare interchangeable inside it.- The scroller's id-bearing slides are the anchor targets AND the snap items. One element serving both roles is why this pattern needs no JavaScript at all.
- UI change from the CSS original: slide 1's dot is now highlighted on first paint via
:not(:has(:target))— the original shipped with all four dots idle and listed the fix only as a gotcha. The accent also darkened from#f59e0btooklch(0.6 0.13 70), which the original could not resolve against white.
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 Dot Pagination Carousel with Target and Has Selectors
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-dot-pagination-carousel-target-has/
A wide-format gallery with indicator dots that jump to their slide and light up when active. The dots are anchor links, the highlight is :target read through :has(), and the whole thing is navigation that works with JavaScript disabled.
## HTML
```html
<section class="tcar-16 w-full min-h-svh grid place-items-center bg-tcar16-bg font-tcar16-sans px-5 py-10" aria-label="Destination gallery">
<div class="group tcar16-frame w-[min(680px,100%)] bg-white rounded-[18px] overflow-hidden shadow-[0_24px_50px_-34px_oklch(0.45_0.1_75/0.5)]">
<ul tabindex="0" role="group" aria-label="Destinations, swipe or use arrow keys" class="tcar16-scroller list-none flex overflow-x-auto m-0 p-0 snap-x snap-mandatory scroll-smooth [scrollbar-width:none] focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:-outline-offset-[3px]">
<li id="tcar-16-1" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-1/1000/560" width="1000" height="560" fetchpriority="high" decoding="async" alt="Amalfi coastline" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Amalfi Coast</span></li>
<li id="tcar-16-2" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-2/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Santorini rooftops" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Santorini</span></li>
<li id="tcar-16-3" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-3/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Kyoto temple garden" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Kyoto</span></li>
<li id="tcar-16-4" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-4/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Patagonia peaks" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Patagonia</span></li>
</ul>
<nav aria-label="Pagination" class="flex justify-center p-1 bg-white">
<a href="#tcar-16-1" aria-label="Go to slide 1" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-1:target]:bg-tcar16-acc group-has-[#tcar-16-1:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-2" aria-label="Go to slide 2" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-2:target]:bg-tcar16-acc group-has-[#tcar-16-2:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-3" aria-label="Go to slide 3" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-3:target]:bg-tcar16-acc group-has-[#tcar-16-3:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-4" aria-label="Go to slide 4" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-4:target]:bg-tcar16-acc group-has-[#tcar-16-4:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
</nav>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcar16-bg: #faf6ee;
--color-tcar16-bg: oklch(0.97 0.012 85);
--color-tcar16-dot: #dcc9a4;
--color-tcar16-dot: oklch(0.85 0.045 85);
--color-tcar16-dothover: #c4a873;
--color-tcar16-dothover: oklch(0.75 0.07 85);
--color-tcar16-acc: #b26a00;
--color-tcar16-acc: oklch(0.6 0.13 70);
--font-tcar16-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-16 {
width: 100%;
min-height: 100svh;
}
.tcar-16 ::selection {
background: var(--color-tcar16-acc);
color: #fff;
}
.tcar16-scroller::-webkit-scrollbar {
display: none;
}
/* First paint: nothing is :target yet, so slide 1 owns the highlight.
:not(:has(:target)) has no readable variant form — this is the rule that stays CSS. */
.tcar16-frame:not(:has(:target)) .tcar16-dot:first-child > span {
background: var(--color-tcar16-acc);
scale: 1.4;
}
@media (prefers-reduced-motion: reduce) {
.tcar16-scroller {
scroll-behavior: auto;
}
}
```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 Dot Pagination Carousel with Target and Has Selectors
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-dot-pagination-carousel-target-has/
A wide-format gallery with indicator dots that jump to their slide and light up when active. The dots are anchor links, the highlight is :target read through :has(), and the whole thing is navigation that works with JavaScript disabled.
## HTML
```html
<section class="tcar-16 w-full min-h-svh grid place-items-center bg-tcar16-bg font-tcar16-sans px-5 py-10" aria-label="Destination gallery">
<div class="group tcar16-frame w-[min(680px,100%)] bg-white rounded-[18px] overflow-hidden shadow-[0_24px_50px_-34px_oklch(0.45_0.1_75/0.5)]">
<ul tabindex="0" role="group" aria-label="Destinations, swipe or use arrow keys" class="tcar16-scroller list-none flex overflow-x-auto m-0 p-0 snap-x snap-mandatory scroll-smooth [scrollbar-width:none] focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:-outline-offset-[3px]">
<li id="tcar-16-1" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-1/1000/560" width="1000" height="560" fetchpriority="high" decoding="async" alt="Amalfi coastline" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Amalfi Coast</span></li>
<li id="tcar-16-2" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-2/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Santorini rooftops" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Santorini</span></li>
<li id="tcar-16-3" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-3/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Kyoto temple garden" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Kyoto</span></li>
<li id="tcar-16-4" class="relative flex-none basis-full snap-center aspect-video overflow-hidden"><img src="https://picsum.photos/seed/dot-4/1000/560" width="1000" height="560" loading="lazy" decoding="async" alt="Patagonia peaks" class="size-full object-cover block"><span class="absolute inset-x-0 bottom-0 px-5 pt-[34px] pb-4 text-white text-lg font-bold bg-[linear-gradient(transparent,oklch(0_0_0/0.7))]">Patagonia</span></li>
</ul>
<nav aria-label="Pagination" class="flex justify-center p-1 bg-white">
<a href="#tcar-16-1" aria-label="Go to slide 1" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-1:target]:bg-tcar16-acc group-has-[#tcar-16-1:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-2" aria-label="Go to slide 2" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-2:target]:bg-tcar16-acc group-has-[#tcar-16-2:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-3" aria-label="Go to slide 3" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-3:target]:bg-tcar16-acc group-has-[#tcar-16-3:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
<a href="#tcar-16-4" aria-label="Go to slide 4" class="tcar16-dot group/dot size-11 grid place-items-center no-underline"><span class="size-2.5 rounded-full bg-tcar16-dot transition-[scale,background-color] duration-200 group-hover/dot:bg-tcar16-dothover focus-visible:outline-3 focus-visible:outline-tcar16-acc focus-visible:outline-offset-[3px] group-has-[#tcar-16-4:target]:bg-tcar16-acc group-has-[#tcar-16-4:target]:scale-[1.4] motion-reduce:transition-none"></span></a>
</nav>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcar16-bg: #faf6ee;
--color-tcar16-bg: oklch(0.97 0.012 85);
--color-tcar16-dot: #dcc9a4;
--color-tcar16-dot: oklch(0.85 0.045 85);
--color-tcar16-dothover: #c4a873;
--color-tcar16-dothover: oklch(0.75 0.07 85);
--color-tcar16-acc: #b26a00;
--color-tcar16-acc: oklch(0.6 0.13 70);
--font-tcar16-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-16 {
width: 100%;
min-height: 100svh;
}
.tcar-16 ::selection {
background: var(--color-tcar16-acc);
color: #fff;
}
.tcar16-scroller::-webkit-scrollbar {
display: none;
}
/* First paint: nothing is :target yet, so slide 1 owns the highlight.
:not(:has(:target)) has no readable variant form — this is the rule that stays CSS. */
.tcar16-frame:not(:has(:target)) .tcar16-dot:first-child > span {
background: var(--color-tcar16-acc);
scale: 1.4;
}
@media (prefers-reduced-motion: reduce) {
.tcar16-scroller {
scroll-behavior: auto;
}
}
```How this works
The fragment is the state. Clicking a dot sets the URL hash, the matching slide becomes :target, and group-has-[#tcar-16-2:target]: finds the dot that should light. No JavaScript, no active-index variable, and the state survives a page reload or a shared link.
The first-paint default stays in CSS. Before any click, no slide is :target and every dot is idle. The fix is :not(:has(:target)) on the frame, which has no clean variant form — two lines of CSS, and the gallery never opens in a stateless-looking condition.
Swipe does not update the fragment. This is inherent to the pattern: touch scrolling changes no URL, so the highlight reflects the last clicked dot. Adding an IntersectionObserver would fix it and would also make this a JS demo.
Make it yours
- Numbered markers: put the index in the anchor text and drop
rounded-full. - Dot row over the image: absolutely position the
<nav>and tint the dots. - Add a slide: a new
<li id>plus a dot with the matching condition. - Colours:
--color-tcar16-acc,--color-tcar16-dot,--color-tcar16-dothover.
Gotchas — read before shipping
- Swipe scrolling never updates the fragment, so the dots track clicks only. Say so in your own docs rather than letting a QA ticket discover it.
scroll-smoothmust be on the scroller, and the page's ownscroll-behaviormatters too — an anchor jump inside a scroller can also scroll the document.- Keep the
<nav aria-label="Pagination">wrapper. Without it, four unlabelled circles are four unlabelled circles. - Enlarge the dot's <b>own</b> box to 44px rather than inflating it with
before:-inset-*: with a 10px dot and a 10px gap the pseudo hit areas overlap by 14px, and the topmost one in paint order takes clicks meant for its neighbours.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() plus :target drive the highlight (Chrome 105+, Safari 15.4+, Firefox 121+). Anchor navigation itself is universal — in an older browser the dots still navigate, only the highlight is lost.