Pure TailwindMIT licensed

Tailwind Cross-Fade Image Slideshow with Manual Controls

A manual slideshow that dissolves between photographs with pure opacity — no sliding, no reflow, no autoplay. The calm alternative to an auto-advancing hero, and the simplest possible demonstration of the group-has- state 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-14 w-full min-h-svh grid place-items-center bg-tcar14-bg font-tcar14-sans px-5 py-10" aria-label="Photo slideshow">
  <div class="group w-[min(620px,100%)]">
    <input type="radio" name="tcar-14" id="tcar-14-1" class="sr-only" checked>
    <input type="radio" name="tcar-14" id="tcar-14-2" class="sr-only">
    <input type="radio" name="tcar-14" id="tcar-14-3" class="sr-only">
    <input type="radio" name="tcar-14" id="tcar-14-4" class="sr-only">
    <div class="relative aspect-[3/2] rounded-2xl overflow-hidden bg-tcar14-ph shadow-[0_26px_50px_-32px_oklch(0_0_0/0.9)]">
      <img src="https://picsum.photos/seed/fade-1/900/600" width="900" height="600" fetchpriority="high" decoding="async" alt="Foggy harbour at dawn" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[800ms] ease-out group-has-[#tcar-14-1:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/fade-2/900/600" width="900" height="600" loading="lazy" decoding="async" alt="Neon-lit alley at night" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[800ms] ease-out group-has-[#tcar-14-2:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/fade-3/900/600" width="900" height="600" loading="lazy" decoding="async" alt="Snowy mountain village" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[800ms] ease-out group-has-[#tcar-14-3:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/fade-4/900/600" width="900" height="600" loading="lazy" decoding="async" alt="Field of lavender" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[800ms] ease-out group-has-[#tcar-14-4:checked]:opacity-100 motion-reduce:transition-none">
    </div>
    <div class="flex gap-2.5 justify-center mt-4">
      <label for="tcar-14-1" aria-label="Slide 1" class="relative w-[34px] h-[5px] rounded-full bg-tcar14-dot cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:-inset-x-1 before:-inset-y-[19px] hover:bg-tcar14-dothover group-has-[#tcar-14-1:checked]:bg-tcar14-acc group-has-[#tcar-14-1:focus-visible]:outline-3 group-has-[#tcar-14-1:focus-visible]:outline-tcar14-acc group-has-[#tcar-14-1:focus-visible]:outline-offset-[6px]"></label>
      <label for="tcar-14-2" aria-label="Slide 2" class="relative w-[34px] h-[5px] rounded-full bg-tcar14-dot cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:-inset-x-1 before:-inset-y-[19px] hover:bg-tcar14-dothover group-has-[#tcar-14-2:checked]:bg-tcar14-acc group-has-[#tcar-14-2:focus-visible]:outline-3 group-has-[#tcar-14-2:focus-visible]:outline-tcar14-acc group-has-[#tcar-14-2:focus-visible]:outline-offset-[6px]"></label>
      <label for="tcar-14-3" aria-label="Slide 3" class="relative w-[34px] h-[5px] rounded-full bg-tcar14-dot cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:-inset-x-1 before:-inset-y-[19px] hover:bg-tcar14-dothover group-has-[#tcar-14-3:checked]:bg-tcar14-acc group-has-[#tcar-14-3:focus-visible]:outline-3 group-has-[#tcar-14-3:focus-visible]:outline-tcar14-acc group-has-[#tcar-14-3:focus-visible]:outline-offset-[6px]"></label>
      <label for="tcar-14-4" aria-label="Slide 4" class="relative w-[34px] h-[5px] rounded-full bg-tcar14-dot cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:-inset-x-1 before:-inset-y-[19px] hover:bg-tcar14-dothover group-has-[#tcar-14-4:checked]:bg-tcar14-acc group-has-[#tcar-14-4:focus-visible]:outline-3 group-has-[#tcar-14-4:focus-visible]:outline-tcar14-acc group-has-[#tcar-14-4:focus-visible]:outline-offset-[6px]"></label>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tcar14-bg: #0f1720;
  --color-tcar14-bg: oklch(0.2 0.02 250);
  --color-tcar14-ph: #1b2733;
  --color-tcar14-ph: oklch(0.26 0.02 250);
  --color-tcar14-dot: #31404f;
  --color-tcar14-dot: oklch(0.36 0.022 250);
  --color-tcar14-dothover: #56697c;
  --color-tcar14-dothover: oklch(0.52 0.03 250);
  --color-tcar14-acc: #38b6f0;
  --color-tcar14-acc: oklch(0.73 0.14 230);
  --font-tcar14-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

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

.tcar-14 ::selection {
  background: var(--color-tcar14-acc);
  color: #062030;
}

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-tcar14-bg: #0f1720;
  --color-tcar14-bg: oklch(0.2 0.02 250);
  --color-tcar14-ph: #1b2733;
  --color-tcar14-ph: oklch(0.26 0.02 250);
  --color-tcar14-dot: #31404f;
  --color-tcar14-dot: oklch(0.36 0.022 250);
  --color-tcar14-dothover: #56697c;
  --color-tcar14-dothover: oklch(0.52 0.03 250);
  --color-tcar14-acc: #38b6f0;
  --color-tcar14-acc: oklch(0.73 0.14 230);
  --font-tcar14-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

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

.tcar-14 ::selection {
  background: var(--color-tcar14-acc);
  color: #062030;
}
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.

  • The purest example of the collection's state pattern: twelve CSS selectors reduce to four utilities, one per image, and there is nothing else going on to distract from the swap.
  • The indicator's hit area is asymmetric — before:-inset-x-1 before:-inset-y-[19px] — because a 34×5px bar needs height, not width, to reach 44px. This is the one control in the collection where a before: hit area is safe: the bars sit on a 44px pitch (34px wide plus a 10px gap), so the boxes tile exactly instead of overlapping. Wherever the pitch is tighter than the target — demos 10, 16 and 22 — the 44px box belongs on the control itself, with the dot centred inside it.
  • UI change from the CSS original: the accent moved from #0ea5e9 to oklch(0.73 0.14 230) and the idle bar lightened. On the dark ground the original active bar and idle bar were close enough in luminance that the current slide was hard to identify at a glance.
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 Cross-Fade Image Slideshow with Manual Controls
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-crossfade-image-slideshow-manual/

A manual slideshow that dissolves between photographs with pure opacity — no sliding, no reflow, no autoplay. The calm alternative to an auto-advancing hero, and the simplest possible demonstration of the group-has- state pattern.
## HTML
```html
<section class="tcar-14 w-full min-h-svh grid place-items-center bg-tcar14-bg font-tcar14-sans px-5 py-10" aria-label="Photo slideshow">
  <div class="group w-[min(620px,100%)]">
    <input type="radio" name="tcar-14" id="tcar-14-1" class="sr-only" checked>
    <input type="radio" name="tcar-14" id="tcar-14-2" class="sr-only">
    <input type="radio" name="tcar-14" id="tcar-14-3" class="sr-only">
    <input type="radio" name="tcar-14" id="tcar-14-4" class="sr-only">
    <div class="relative aspect-[3/2] rounded-2xl overflow-hidden bg-tcar14-ph shadow-[0_26px_50px_-32px_oklch(0_0_0/0.9)]">
      <img src="https://picsum.photos/seed/fade-1/900/600" width="900" height="600" fetchpriority="high" decoding="async" alt="Foggy harbour at dawn" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[800ms] ease-out group-has-[#tcar-14-1:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/fade-2/900/600" width="900" height="600" loading="lazy" decoding="async" alt="Neon-lit alley at night" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[800ms] ease-out group-has-[#tcar-14-2:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/fade-3/900/600" width="900" height="600" loading="lazy" decoding="async" alt="Snowy mountain village" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[800ms] ease-out group-has-[#tcar-14-3:checked]:opacity-100 motion-reduce:transition-none">
      <img src="https://picsum.photos/seed/fade-4/900/600" width="900" height="600" loading="lazy" decoding="async" alt="Field of lavender" class="absolute inset-0 size-full object-cover opacity-0 transition-opacity duration-[800ms] ease-out group-has-[#tcar-14-4:checked]:opacity-100 motion-reduce:transition-none">
    </div>
    <div class="flex gap-2.5 justify-center mt-4">
      <label for="tcar-14-1" aria-label="Slide 1" class="relative w-[34px] h-[5px] rounded-full bg-tcar14-dot cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:-inset-x-1 before:-inset-y-[19px] hover:bg-tcar14-dothover group-has-[#tcar-14-1:checked]:bg-tcar14-acc group-has-[#tcar-14-1:focus-visible]:outline-3 group-has-[#tcar-14-1:focus-visible]:outline-tcar14-acc group-has-[#tcar-14-1:focus-visible]:outline-offset-[6px]"></label>
      <label for="tcar-14-2" aria-label="Slide 2" class="relative w-[34px] h-[5px] rounded-full bg-tcar14-dot cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:-inset-x-1 before:-inset-y-[19px] hover:bg-tcar14-dothover group-has-[#tcar-14-2:checked]:bg-tcar14-acc group-has-[#tcar-14-2:focus-visible]:outline-3 group-has-[#tcar-14-2:focus-visible]:outline-tcar14-acc group-has-[#tcar-14-2:focus-visible]:outline-offset-[6px]"></label>
      <label for="tcar-14-3" aria-label="Slide 3" class="relative w-[34px] h-[5px] rounded-full bg-tcar14-dot cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:-inset-x-1 before:-inset-y-[19px] hover:bg-tcar14-dothover group-has-[#tcar-14-3:checked]:bg-tcar14-acc group-has-[#tcar-14-3:focus-visible]:outline-3 group-has-[#tcar-14-3:focus-visible]:outline-tcar14-acc group-has-[#tcar-14-3:focus-visible]:outline-offset-[6px]"></label>
      <label for="tcar-14-4" aria-label="Slide 4" class="relative w-[34px] h-[5px] rounded-full bg-tcar14-dot cursor-pointer transition-colors duration-250 before:content-[''] before:absolute before:-inset-x-1 before:-inset-y-[19px] hover:bg-tcar14-dothover group-has-[#tcar-14-4:checked]:bg-tcar14-acc group-has-[#tcar-14-4:focus-visible]:outline-3 group-has-[#tcar-14-4:focus-visible]:outline-tcar14-acc group-has-[#tcar-14-4:focus-visible]:outline-offset-[6px]"></label>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcar14-bg: #0f1720;
  --color-tcar14-bg: oklch(0.2 0.02 250);
  --color-tcar14-ph: #1b2733;
  --color-tcar14-ph: oklch(0.26 0.02 250);
  --color-tcar14-dot: #31404f;
  --color-tcar14-dot: oklch(0.36 0.022 250);
  --color-tcar14-dothover: #56697c;
  --color-tcar14-dothover: oklch(0.52 0.03 250);
  --color-tcar14-acc: #38b6f0;
  --color-tcar14-acc: oklch(0.73 0.14 230);
  --font-tcar14-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

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

.tcar-14 ::selection {
  background: var(--color-tcar14-acc);
  color: #062030;
}
```

How this works

Only opacity moves. Stacked absolutely-positioned images, one visible. Nothing is translated and nothing is laid out, so the dissolve is free and CLS is structurally zero.

Manual by design. There is no timeline here at all — the user steps at their own pace with a click or an arrow key. For a gallery of unrelated photographs that is the accessible default; autoplay is demo 20's problem.

One slide must start checked. checked on the first radio is the difference between a slideshow and an empty grey box on first paint.

Make it yours

  • Dissolve speed: duration-[800ms] on the images.
  • Add a slide: one radio, one image, one bar, each with its own condition.
  • Add a slow zoom: group-has-[...]:scale-105 alongside the opacity.
  • Prev/next labels instead of bars, pointed at specific radio ids.

Gotchas — read before shipping

  • Exactly one radio carries checked at load. Zero means a blank frame; two is invalid markup and the browser picks for you.
  • aspect-[3/2] on the frame, not on the images — the images are absolutely positioned and have no say in the box.
  • Do not add autoplay to this pattern without a pause control; a dissolve the user cannot stop is worse than one they must advance.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Opacity transitions are universal; :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

Search CodeFronts

Loading…