CSS Snippets

Copy. Paste. Ship.

Home › Code Snippets › CSS › Image Slider

Part of our complete guide: CSS Snippets →

UI Component · HTML + CSS

CSS Image Slider

Three carousel patterns — fade, slide, and :target-controlled. Two are pure CSS with zero JavaScript. One uses anchor targets for manual navigation.

Last updated: October 2026 · 8 min read

A CSS image slider cycles through images using @keyframes animations. Auto-advancing sliders are pure CSS. Manual navigation (arrows, dots) needs either JavaScript or the CSS :target trick.

What you get: Three complete patterns — fade (opacity), slide (transform), and :target (manual click). All are copy-paste ready and mobile-friendly.

Why CSS sliders matter

Image sliders used to require a JavaScript library — Slick, Swiper, Owl Carousel, each weighing 30-60KB and adding dozens of features most sites never use. Modern CSS can handle the visual sliding entirely. The only thing CSS can't do is detect user clicks or swipes.

Pure CSS sliders are faster, lighter, and work even when JavaScript is disabled. The trade-off: no swipe gestures on mobile, no "pause on hover" (unless you add it), and limited accessibility without JavaScript. For auto-rotating hero sliders, they're ideal. For interactive galleries where users browse manually, add a small amount of JS or use the :target pattern.

The three patterns below cover the most common cases. The fade and slide versions auto-rotate every few seconds. The :target version lets users click dots to navigate — no JavaScript, just anchor links and CSS.

🎬 Live Sliders

All three auto-advance or accept clicks. Copy the code for each.

🌅 Fade Slider

🎨
🌅
🌊
<div class="slider-fade">
  <div class="slide">🎨</div>
  <div class="slide">🌅</div>
  <div class="slide">🌊</div>
</div>

<style>
.slider-fade {
  position: relative;
  width: 100%; max-width: 300px;
  height: 180px; border-radius: 12px;
  overflow: hidden; background: #1e293b;
}
.slider-fade .slide {
  position: absolute; inset: 0;
  opacity: 0;
  animation: fadeCycle 9s infinite;
  display: flex; align-items: center;
  justify-content: center; font-size: 48px;
}
.slider-fade .slide:nth-child(1) {
  animation-delay: 0s;
  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
}
.slider-fade .slide:nth-child(2) {
  animation-delay: 3s;
  background: linear-gradient(135deg, #f59e0b, #ef4444);
}
.slider-fade .slide:nth-child(3) {
  animation-delay: 6s;
  background: linear-gradient(135deg, #10b981, #06b6d4);
}
@keyframes fadeCycle {
  0%, 30% { opacity: 1; }
  33%, 100% { opacity: 0; }
}
</style>

➡️ Slide Carousel

🎨
🌅
🌊
<div class="slider-slide">
  <div class="slider-slide-track">
    <div>🎨</div>
    <div>🌅</div>
    <div>🌊</div>
  </div>
</div>

<style>
.slider-slide {
  position: relative;
  width: 100%; max-width: 300px;
  height: 180px; border-radius: 12px;
  overflow: hidden;
}
.slider-slide-track {
  display: flex; width: 300%; height: 100%;
  animation: slideCycle 12s infinite;
}
.slider-slide-track > div {
  width: 33.333%; flex-shrink: 0;
  display: flex; align-items: center;
  justify-content: center; font-size: 48px;
}
.slider-slide-track > div:nth-child(1) {
  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
}
.slider-slide-track > div:nth-child(2) {
  background: linear-gradient(135deg, #f59e0b, #ef4444);
}
.slider-slide-track > div:nth-child(3) {
  background: linear-gradient(135deg, #10b981, #06b6d4);
}
@keyframes slideCycle {
  0%, 30% { transform: translateX(0); }
  33%, 63% { transform: translateX(-33.333%); }
  66%, 96% { transform: translateX(-66.666%); }
  100% { transform: translateX(0); }
}
</style>

👆 Manual (:target)

🎨
🌅
🌊
<div class="slider-target">
  <div class="slide" id="slide-1">🎨</div>
  <div class="slide" id="slide-2">🌅</div>
  <div class="slide" id="slide-3">🌊</div>
  <div class="slider-dots">
    <a href="#slide-1">•</a>
    <a href="#slide-2">•</a>
    <a href="#slide-3">•</a>
  </div>
</div>

<style>
.slider-target {
  position: relative;
  width: 100%; max-width: 300px;
  height: 180px; border-radius: 12px;
  overflow: hidden;
}
.slider-target .slide {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity 0.4s ease;
  display: flex; align-items: center;
  justify-content: center; font-size: 48px;
}
.slider-target .slide:first-child,
.slider-target .slide:target {
  opacity: 1; z-index: 1;
}
.slider-target .slide:nth-child(1) {
  background: linear-gradient(135deg, #3b82f6, #8b5cf6);
}
.slider-target .slide:nth-child(2) {
  background: linear-gradient(135deg, #f59e0b, #ef4444);
}
.slider-target .slide:nth-child(3) {
  background: linear-gradient(135deg, #10b981, #06b6d4);
}
.slider-target .slider-dots {
  position: absolute; bottom: 10px; left: 50%;
  transform: translateX(-50%);
  display: flex; gap: 8px; z-index: 2;
}
.slider-target .slider-dots a {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: rgba(255,255,255,0.5);
  text-decoration: none; font-size: 0;
}
.slider-target .slider-dots a:hover {
  background: #fff;
}
</style>

How each pattern works

1. Fade — opacity animation

All slides stack on top of each other with position: absolute. Each slide fades in and out on a 9-second cycle, offset by 3 seconds each. At any given moment, only one slide has opacity: 1. The keyframes at 0%, 30% and 33%, 100% control the visible window — 30% of 9s = 2.7s visible, then 0.3s transition, then hidden.

2. Slide — translateX animation

The track is 300% wide (3 slides side by side). The animation shifts it left by 33.333% each cycle, revealing the next slide. At the end, it snaps back to translateX(0). The 30% of each keyframe shows the slide stationary, then the transition happens in the remaining 3%. All animation is transform, which is GPU-accelerated.

3. Manual — :target pseudo-class

Each slide has an id (slide-1, slide-2, slide-3). Each dot links to that id. When you click a dot, the URL becomes #slide-2 and CSS's :target pseudo-class matches the slide with id="slide-2", making it visible. The :first-child selector ensures slide-1 shows by default when no target is active. No JavaScript — just anchor links and CSS.

🔬 Edge cases

The problems that break sliders in production.

Layout shift on page load

Before images load, the slider has zero height. When the first image loads, everything below jumps down. Fix: set a fixed height or aspect-ratio on the slider container. This prevents Cumulative Layout Shift (CLS) — a Core Web Vital.

First frame flash

On page load, all slides briefly show at once (before animations start). Fix: set opacity: 0 on all slides by default, then let the animation set opacity: 1 on the active slide. The fade pattern above already does this.

Reduced motion users

Some users enable "reduce motion" — auto-advancing sliders can trigger motion sickness. Fix:

@media (prefers-reduced-motion: reduce) {
  .slider-fade .slide,
  .slider-slide-track {
    animation: none;
  }
  /* Show first slide only */
  .slider-fade .slide:first-child { opacity: 1; }
}
:target breaks browser history

Each dot click in the :target pattern adds a history entry. Users clicking "Back" in the browser trigger slide changes. This can be surprising. Fix: use history.replaceState(null, '', '#slide-2') on click to replace instead of push. Requires JS but is one line.

Slide disappears at end of animation

If your last keyframe is opacity: 0 and there's no reset, the slider goes blank on the final frame. Fix: make the animation loop match — final keyframe should return to the initial state (opacity: 1 for the first slide).

Autoplay pauses on hover

Good UX: pause the animation when the user hovers over the slider. Fix with pure CSS:

.slider-fade:hover .slide,
.slider-slide:hover .slider-slide-track {
  animation-play-state: paused;
}
No swipe on mobile

Pure CSS sliders don't respond to finger swipes. Users expect to swipe. If swipe-to-advance matters, add 20 lines of JS with touchstart and touchend events, or use scroll-snap which gets native swipe for free:

.slider-scroll {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}
.slider-scroll > div {
  scroll-snap-align: start;
  flex-shrink: 0;
}
Images don't lazy load

Sliders often load 5-10 large images upfront, all before the user sees them. Fix: add loading="lazy" to images except the first one. The first image loads eagerly; the rest load as they become visible.

⚡ Performance

Pattern Animates CPU cost JS
FadeopacityVery lowNone
SlidetransformLowNone
:targetopacity + transitionVery lowNone

The main cost is images, not animation. A 5-slide slider loading 500KB of unoptimized JPEGs is slower than the animations themselves. Compress images, use WebP format, and set loading="lazy" on all but the first slide.

⚠️ Common mistakes

❓ Frequently asked questions

Can I make an image slider with only CSS?

Yes. Auto-scrolling sliders and hover-triggered sliders work with pure CSS using @keyframes and animation properties. Manual navigation (arrows, dots) requires JavaScript or the :target/:checked CSS hacks.

Which slider type is best for performance?

Fade sliders are the most performant. They animate opacity only. Slide/carousel versions that animate transform are also fast. Avoid animating width, height, or margin — those trigger layout and jank.

How do I make a slider that auto-advances?

Use a CSS animation with @keyframes that changes the visible slide at intervals (e.g., 5s per slide). Combine animation-duration with animation-iteration-count: infinite. No JavaScript needed.

Why does my slider flicker on page load?

The first slide loads before others, or images aren't preloaded. Fix: preload images with <link rel=preload>, or set a fixed height on the slider container so the layout doesn't shift between slides.

Can a CSS slider work on mobile?

Yes. Pure CSS sliders work on mobile because they don't depend on hover. But you need to consider touch gestures for swipe-to-advance, which requires JavaScript. For auto-advancing sliders, mobile works fine.

How do I add navigation arrows without JavaScript?

Use the :target pseudo-class and anchor links. Each slide has an id, and the arrows link to the previous/next slide. Only one target can be active at a time, so this works without JS. But it adds history entries to the browser.

What's the difference between a slider and a carousel?

In practice, they're the same thing. A slider shows one item at a time; a carousel shows multiple items or has rotating content. The CSS and HTML patterns are identical.

How do I make the slider accessible?

Add aria-label='Image carousel', pause auto-advance on hover/focus, respect prefers-reduced-motion, provide alt text on images, and make sure keyboard users can control the slider. Pure CSS sliders typically fail some of these tests.

🔗 Related snippets

🌐 External resources

← All CSS Snippets