cyberangles guide

How to Create a CSS-Only Image Carousel

Image carousels (or sliders) are a popular UI component for showcasing multiple images or content in a limited space. Traditionally, carousels rely on JavaScript for interactivity, but with modern CSS features like pseudo-classes, sibling selectors, and transitions, you can build a fully functional carousel using **only HTML and CSS**. A CSS-only carousel offers several benefits: it’s lightweight (no external JS dependencies), improves performance (fewer reflows/repaints), and works in environments where JavaScript is disabled. While it lacks advanced features like dynamic content loading or complex animations, it’s perfect for simple use cases like product showcases, portfolios, or banner ads. In this guide, we’ll walk through building a CSS-only carousel step-by-step, from structuring the HTML to adding customizations like navigation dots and smooth transitions.

Table of Contents

  1. Prerequisites
  2. HTML Structure: The Foundation
  3. CSS Styling: Layout and Basics
  4. Adding Interactivity with CSS Pseudo-Classes
  5. Smooth Transitions
  6. Customizations
  7. Troubleshooting Common Issues
  8. Conclusion
  9. References

Prerequisites

Before starting, ensure you have:

  • Basic knowledge of HTML and CSS (selectors, flexbox, positioning).
  • A text editor (e.g., VS Code) and a modern browser (Chrome, Firefox, Safari, Edge).
  • 3-5 images for the carousel (we’ll use 3 in this example).

HTML Structure: The Foundation

The core of our carousel relies on HTML radio buttons and labels. Radio buttons let us track the “active” slide (via the :checked pseudo-class), and labels act as clickable navigation. Here’s the structure:

<div class="carousel">
  <!-- Radio inputs (hidden) to track active slide -->
  <input type="radio" name="carousel" id="slide1" checked>
  <input type="radio" name="carousel" id="slide2">
  <input type="radio" name="carousel" id="slide3">

  <!-- Slides wrapper: holds all slides -->
  <div class="carousel-slides">
    <div class="carousel-slide">
      <img src="slide1.jpg" alt="Description of slide 1"> <!-- Replace with your image -->
    </div>
    <div class="carousel-slide">
      <img src="slide2.jpg" alt="Description of slide 2">
    </div>
    <div class="carousel-slide">
      <img src="slide3.jpg" alt="Description of slide 3">
    </div>
  </div>

  <!-- Navigation: Labels for radio inputs (will become dots) -->
  <div class="carousel-nav">
    <label for="slide1" class="nav-dot"></label>
    <label for="slide2" class="nav-dot"></label>
    <label for="slide3" class="nav-dot"></label>
  </div>
</div>

Key Components Explained:

  • Radio Inputs: Hidden by CSS, these track which slide is active. The name="carousel" ensures only one radio is checked at a time.
  • Slides Wrapper: A container for all slides, which we’ll transform to “slide” images horizontally.
  • Slides: Individual images wrapped in div.carousel-slide.
  • Navigation Dots: Labels linked to radio inputs (via for="slideX"), which users click to switch slides.

CSS Styling: Layout and Basics

Now, let’s style the carousel to control layout, visibility, and positioning. Add this CSS to your stylesheet:

/* Carousel Container */
.carousel {
  position: relative; /* For absolute positioning of navigation */
  width: 80%; /* Responsive width */
  max-width: 800px; /* Max container size */
  margin: 2rem auto; /* Center on page */
  overflow: hidden; /* Hide slides outside the container */
  border-radius: 12px; /* Rounded corners (optional) */
  box-shadow: 0 4px 12px rgba(0,0,0,0.1); /* Subtle shadow (optional) */
}

/* Slides Wrapper */
.carousel-slides {
  display: flex; /* Arrange slides in a row */
  height: 400px; /* Fixed height (adjust as needed) */
}

/* Individual Slides */
.carousel-slide {
  min-width: 100%; /* Each slide takes full width of the container */
}

/* Slide Images */
.carousel-slide img {
  width: 100%; /* Fill slide width */
  height: 100%; /* Fill slide height */
  object-fit: cover; /* Crop image to fit without distortion */
}

/* Hide Radio Inputs (we’ll use labels for navigation) */
.carousel input {
  display: none;
}

What This Does:

  • The container uses overflow: hidden to ensure only the active slide is visible.
  • display: flex on .carousel-slides arranges slides in a horizontal row.
  • min-width: 100% on slides forces each to take the full width of the container, stacking them horizontally.

Adding Interactivity with CSS Pseudo-Classes

To switch slides when a user clicks a navigation dot, we’ll use the :checked pseudo-class and sibling selectors. When a radio input is checked, we’ll shift the slides wrapper horizontally to reveal the corresponding slide.

Add this to your CSS:

/* Slide Positioning: Shift wrapper based on checked radio */
#slide1:checked ~ .carousel-slides {
  transform: translateX(0); /* Show first slide */
}

#slide2:checked ~ .carousel-slides {
  transform: translateX(-100%); /* Show second slide (shift left by 100%) */
}

#slide3:checked ~ .carousel-slides {
  transform: translateX(-200%); /* Show third slide (shift left by 200%) */
}

How It Works:

  • The ~ (general sibling) selector targets .carousel-slides when a radio input (e.g., #slide2) is checked.
  • transform: translateX(-100%) shifts the slides wrapper left by 100% of its width, revealing the second slide. For the third slide, we shift left by 200%, and so on.

Smooth Transitions

Without transitions, slides will “jump” abruptly. Add a smooth transition to make the switch feel polished:

/* Add smooth transition when sliding */
.carousel-slides {
  transition: transform 0.5s ease-in-out; /* Adjust duration/easing as needed */
}

Now, switching slides will animate smoothly over 0.5 seconds!

Customizations

Let’s style the labels as clickable dots (instead of invisible elements). Update the .carousel-nav and .nav-dot CSS:

/* Navigation Dots Container */
.carousel-nav {
  position: absolute; /* Position dots over the carousel */
  bottom: 20px; /* Distance from bottom */
  left: 50%; /* Center horizontally */
  transform: translateX(-50%); /* Fine-tune centering */
  display: flex; /* Arrange dots in a row */
  gap: 10px; /* Space between dots */
  z-index: 10; /* Ensure dots appear above slides */
}

/* Dot Styling */
.nav-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%; /* Make dots circular */
  background: rgba(255, 255, 255, 0.5); /* Semi-transparent white */
  cursor: pointer; /* Show pointer on hover */
  transition: background 0.3s ease; /* Smooth color change */
}

/* Active Dot: Darken when its radio is checked */
#slide1:checked ~ .carousel-nav .nav-dot[for="slide1"],
#slide2:checked ~ .carousel-nav .nav-dot[for="slide2"],
#slide3:checked ~ .carousel-nav .nav-dot[for="slide3"] {
  background: white; /* Solid white for active dot */
  transform: scale(1.2); /* Slight scale-up for emphasis (optional) */
}

Now you’ll see white dots at the bottom of the carousel, with the active slide’s dot highlighted!

Previous/Next Buttons (Advanced)

Adding “Previous” and “Next” buttons is trickier with CSS (no “previous sibling” selector), but we can approximate it using absolute positioning and labels.

Add these labels to your HTML (inside .carousel):

<!-- Previous/Next Buttons -->
<label for="slide3" class="carousel-btn prev"></label> <!-- "Prev" for slide 1 (wraps to last) -->
<label for="slide2" class="carousel-btn prev"></label> <!-- "Prev" for slide 2 -->
<label for="slide1" class="carousel-btn prev"></label> <!-- "Prev" for slide 3 -->

<label for="slide2" class="carousel-btn next"></label> <!-- "Next" for slide 1 -->
<label for="slide3" class="carousel-btn next"></label> <!-- "Next" for slide 2 -->
<label for="slide1" class="carousel-btn next"></label> <!-- "Next" for slide 3 (wraps to first) -->

Style the buttons with CSS:

/* Previous/Next Buttons */
.carousel-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 50px;
  height: 50px;
  background: rgba(0,0,0,0.3);
  border-radius: 50%;
  cursor: pointer;
  z-index: 10;
}

/* Position buttons */
.prev { left: 20px; }
.next { right: 20px; }

/* Add arrow icons (optional) */
.prev::after, .next::after {
  content: "";
  position: absolute;
  width: 15px;
  height: 15px;
  border-top: 3px solid white;
  border-left: 3px solid white;
  top: 50%;
  left: 50%;
}

.next::after {
  transform: translate(-30%, -50%) rotate(135deg); /* Right arrow */
}

.prev::after {
  transform: translate(-70%, -50%) rotate(-45deg); /* Left arrow */
}

Limitations: This requires hardcoding labels for each slide, making it impractical for carousels with dynamic content. For flexibility, use JavaScript instead.

Autoplay (Limitations)

You can auto-advance slides with CSS animations, but it won’t sync with navigation dots. Add this to .carousel-slides:

/* Autoplay animation (optional) */
@keyframes autoplay {
  0% { transform: translateX(0); }
  33% { transform: translateX(-100%); }
  66% { transform: translateX(-200%); }
  100% { transform: translateX(0); }
}

.carousel-slides {
  animation: autoplay 9s infinite; /* 3 slides × 3s per slide */
}

/* Pause autoplay on hover (optional) */
.carousel:hover .carousel-slides {
  animation-play-state: paused;
}

Caveat: Navigation dots won’t update to reflect the auto-played slide (since the radio inputs aren’t being checked). Use this only for simple, auto-only carousels.

Troubleshooting

  • Slides not showing: Ensure overflow: hidden is set on the container and min-width: 100% is on slides.
  • Transitions not working: Check that transition is applied to .carousel-slides, not individual slides.
  • Navigation dots unclickable: Verify label elements have for="slideX" matching radio input IDs.
  • Slides distorted: Use object-fit: cover (or contain) on images to prevent stretching.

Conclusion

You’ve built a fully functional CSS-only image carousel! This approach is lightweight, easy to implement, and perfect for simple use cases. While it lacks advanced features like dynamic content or keyboard navigation, it’s a great way to reduce dependencies and improve performance.

For complex carousels (e.g., with autoplay + sync’d dots or swipe support), consider adding JavaScript. But for most static showcases, this CSS-only solution works beautifully.

References