Pure TailwindMIT licensed

Tailwind Autoplay Slider with Keyframe Track and Progress Bars

A hands-off banner that advances itself by translating one track through each slide on a keyframe loop, with progress bars that fill in step. Two animation tokens, three arbitrary delays, and no timer to leak or desync in a background tab.

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-20 w-full min-h-svh grid place-items-center bg-tcar20-bg font-tcar20-sans px-5 py-10" aria-label="Auto-rotating banner" aria-roledescription="carousel">
  <div class="group relative w-[min(680px,100%)] rounded-2xl overflow-hidden shadow-[0_26px_50px_-32px_oklch(0_0_0/0.9)]">
    <div class="flex w-[300%] animate-tcar20-slide group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused] motion-reduce:animate-none">
      <div class="w-1/3 aspect-[2/1]"><img src="https://picsum.photos/seed/auto-1/1000/500" width="1000" height="500" fetchpriority="high" decoding="async" alt="Summer sale banner" class="size-full object-cover block"></div>
      <div class="w-1/3 aspect-[2/1]"><img src="https://picsum.photos/seed/auto-2/1000/500" width="1000" height="500" loading="lazy" decoding="async" alt="New arrivals banner" class="size-full object-cover block"></div>
      <div class="w-1/3 aspect-[2/1]"><img src="https://picsum.photos/seed/auto-3/1000/500" width="1000" height="500" loading="lazy" decoding="async" alt="Free shipping banner" class="size-full object-cover block"></div>
    </div>
    <div aria-hidden="true" class="absolute inset-x-3.5 bottom-3.5 flex gap-2 motion-reduce:hidden">
      <span class="relative flex-1 h-1 rounded-full bg-tcar20-track shadow-[0_0_0_1px_oklch(0_0_0/0.28)] overflow-hidden"><span class="absolute inset-0 bg-tcar20-acc origin-left scale-x-0 animate-tcar20-bar group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused]"></span></span>
      <span class="relative flex-1 h-1 rounded-full bg-tcar20-track shadow-[0_0_0_1px_oklch(0_0_0/0.28)] overflow-hidden"><span class="absolute inset-0 bg-tcar20-acc origin-left scale-x-0 animate-tcar20-bar [animation-delay:-10s] group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused]"></span></span>
      <span class="relative flex-1 h-1 rounded-full bg-tcar20-track shadow-[0_0_0_1px_oklch(0_0_0/0.28)] overflow-hidden"><span class="absolute inset-0 bg-tcar20-acc origin-left scale-x-0 animate-tcar20-bar [animation-delay:-5s] group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused]"></span></span>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tcar20-bg: #1a0f18;
  --color-tcar20-bg: oklch(0.21 0.035 340);
  --color-tcar20-track: #ffffff;
  --color-tcar20-track: oklch(1 0 0 / 0.4);
  --color-tcar20-acc: #ea4c95;
  --color-tcar20-acc: oklch(0.68 0.2 350);
  --font-tcar20-sans: 'Archivo', system-ui, sans-serif;
  --animate-tcar20-slide: tcar20-slide 15s infinite;
  --animate-tcar20-bar: tcar20-bar 15s infinite;
}

@keyframes tcar20-slide {
  0%,
    28% {
    transform: translateX(0);
  }

  33%,
    61% {
    transform: translateX(-33.3333%);
  }

  66%,
    94% {
    transform: translateX(-66.6666%);
  }

  100% {
    transform: translateX(0);
  }
}

@keyframes tcar20-bar {
  0% {
    transform: scaleX(0);
  }

  28% {
    transform: scaleX(1);
  }

  33%,
    100% {
    transform: scaleX(0);
  }
}

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

.tcar-20 ::selection {
  background: var(--color-tcar20-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-tcar20-bg: #1a0f18;
  --color-tcar20-bg: oklch(0.21 0.035 340);
  --color-tcar20-track: #ffffff;
  --color-tcar20-track: oklch(1 0 0 / 0.4);
  --color-tcar20-acc: #ea4c95;
  --color-tcar20-acc: oklch(0.68 0.2 350);
  --font-tcar20-sans: 'Archivo', system-ui, sans-serif;
  --animate-tcar20-slide: tcar20-slide 15s infinite;
  --animate-tcar20-bar: tcar20-bar 15s infinite;
}

@keyframes tcar20-slide {
  0%,
    28% {
    transform: translateX(0);
  }

  33%,
    61% {
    transform: translateX(-33.3333%);
  }

  66%,
    94% {
    transform: translateX(-66.6666%);
  }

  100% {
    transform: translateX(0);
  }
}

@keyframes tcar20-bar {
  0% {
    transform: scaleX(0);
  }

  28% {
    transform: scaleX(1);
  }

  33%,
    100% {
    transform: scaleX(0);
  }
}

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

.tcar-20 ::selection {
  background: var(--color-tcar20-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=Archivo:wght@400;600;700;800&display=swap">

What changed in the port

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

  • Two @theme animation tokens sharing one duration is what keeps the track and the bars honest. Change 15s in one and the component desyncs immediately — which is an argument for a shared --duration value the moment a third animation joins.
  • The bar fill animates transform: scaleX() from a origin-left element, never width. Three width animations running continuously behind a banner would be a measurable INP cost.
  • UI change from the CSS original: the bars are motion-reduce:hidden rather than display:none inside a media query, and the accent lifted from #db2777 to oklch(0.68 0.2 350) so the 4px fill is visible against the translucent white track on bright banner artwork.
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 Autoplay Slider with Keyframe Track and Progress Bars
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-autoplay-slider-keyframe-track/

A hands-off banner that advances itself by translating one track through each slide on a keyframe loop, with progress bars that fill in step. Two animation tokens, three arbitrary delays, and no timer to leak or desync in a background tab.
## HTML
```html
<section class="tcar-20 w-full min-h-svh grid place-items-center bg-tcar20-bg font-tcar20-sans px-5 py-10" aria-label="Auto-rotating banner" aria-roledescription="carousel">
  <div class="group relative w-[min(680px,100%)] rounded-2xl overflow-hidden shadow-[0_26px_50px_-32px_oklch(0_0_0/0.9)]">
    <div class="flex w-[300%] animate-tcar20-slide group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused] motion-reduce:animate-none">
      <div class="w-1/3 aspect-[2/1]"><img src="https://picsum.photos/seed/auto-1/1000/500" width="1000" height="500" fetchpriority="high" decoding="async" alt="Summer sale banner" class="size-full object-cover block"></div>
      <div class="w-1/3 aspect-[2/1]"><img src="https://picsum.photos/seed/auto-2/1000/500" width="1000" height="500" loading="lazy" decoding="async" alt="New arrivals banner" class="size-full object-cover block"></div>
      <div class="w-1/3 aspect-[2/1]"><img src="https://picsum.photos/seed/auto-3/1000/500" width="1000" height="500" loading="lazy" decoding="async" alt="Free shipping banner" class="size-full object-cover block"></div>
    </div>
    <div aria-hidden="true" class="absolute inset-x-3.5 bottom-3.5 flex gap-2 motion-reduce:hidden">
      <span class="relative flex-1 h-1 rounded-full bg-tcar20-track shadow-[0_0_0_1px_oklch(0_0_0/0.28)] overflow-hidden"><span class="absolute inset-0 bg-tcar20-acc origin-left scale-x-0 animate-tcar20-bar group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused]"></span></span>
      <span class="relative flex-1 h-1 rounded-full bg-tcar20-track shadow-[0_0_0_1px_oklch(0_0_0/0.28)] overflow-hidden"><span class="absolute inset-0 bg-tcar20-acc origin-left scale-x-0 animate-tcar20-bar [animation-delay:-10s] group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused]"></span></span>
      <span class="relative flex-1 h-1 rounded-full bg-tcar20-track shadow-[0_0_0_1px_oklch(0_0_0/0.28)] overflow-hidden"><span class="absolute inset-0 bg-tcar20-acc origin-left scale-x-0 animate-tcar20-bar [animation-delay:-5s] group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused]"></span></span>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcar20-bg: #1a0f18;
  --color-tcar20-bg: oklch(0.21 0.035 340);
  --color-tcar20-track: #ffffff;
  --color-tcar20-track: oklch(1 0 0 / 0.4);
  --color-tcar20-acc: #ea4c95;
  --color-tcar20-acc: oklch(0.68 0.2 350);
  --font-tcar20-sans: 'Archivo', system-ui, sans-serif;
  --animate-tcar20-slide: tcar20-slide 15s infinite;
  --animate-tcar20-bar: tcar20-bar 15s infinite;
}

@keyframes tcar20-slide {
  0%,
    28% {
    transform: translateX(0);
  }

  33%,
    61% {
    transform: translateX(-33.3333%);
  }

  66%,
    94% {
    transform: translateX(-66.6666%);
  }

  100% {
    transform: translateX(0);
  }
}

@keyframes tcar20-bar {
  0% {
    transform: scaleX(0);
  }

  28% {
    transform: scaleX(1);
  }

  33%,
    100% {
    transform: scaleX(0);
  }
}

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

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

How this works

The animation is the scheduler. One keyframe holds each slide then steps the track, looping forever. There is no setInterval, which means nothing to clear, nothing to desync after a tab sleeps, and no work at all when the section is off-screen.

The bars are the same trick, offset. Each bar's fill runs the same 15s keyframe started at a different negative delay, so it completes exactly while its slide is showing. Keeping duration and delays in one arithmetic family is what holds them in sync.

Pausing must cover both. If the track pauses and the bars keep filling, the component immediately looks broken — so the same variant is on both.

Make it yours

  • Dwell and cycle: 15s in both tokens, kept in step with the keyframe stops.
  • Add a slide: widen the track to w-[400%], add a keyframe stop, add a bar with its delay.
  • Continuous drift: ease the keyframe instead of holding.
  • Restyle or remove the bars — they are independent of the track.

Gotchas — read before shipping

  • Track width must be an exact multiple of 100%, and the keyframe percentages must divide evenly by the slide count, or the timing drifts a little further every loop.
  • Always pause on hover and focus. An unstoppable auto-rotating banner is an accessibility failure and an INP liability.
  • Under reduced motion, hide the bars rather than freezing them — a progress bar stuck at 40% is worse than no progress bar.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Keyframe translation needs nothing modern; the floor quoted is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Plain CSSno Tailwind surface

Search CodeFronts

Loading…