Experience the powerful AI writing right inside WordPress
Show stunning before-and-after transformations with image sliders.
Improve user engagement by showing estimated reading time.
Written by Mahmuda Akter Isha
Showcase Designs Using Before After Slider.
You can create an image slider without JavaScript by placing the images inside a Flexbox track and using CSS transform rules to move that track. Radio buttons can control a manual slider, while @keyframes can produce automatic slide transitions.
transform
@keyframes
An image slider is an effective way to display product photos, portfolio projects, featured content, travel images, or promotional banners without taking up too much space.
Although many sliders depend on JavaScript, you can build a simple and responsive image slider using only HTML and CSS. CSS radio buttons can control a manual slider, while CSS keyframe animations can create an automatic slideshow.
In this blog, you will learn how to create:
You will also get complete source code that you can copy, customize, and use in your own project.
An image slider, also called an image carousel or slideshow, displays a collection of images within the same section of a webpage.
Instead of placing every image vertically on the page, the slider shows one image at a time. Visitors can select another slide using navigation dots, arrows, thumbnails, or automatic transitions.
Image sliders are commonly used for:
The slider in this tutorial is a standard image carousel. It is different from a before-and-after comparison slider, which displays two versions of the same image using a draggable comparison handle.
Yes. A basic image slider can be created using only HTML and CSS.
For a manual slider, hidden radio buttons store the currently selected slide. Each navigation label connects to one radio button. When a visitor selects a radio button, a CSS sibling selector moves the slide track to the correct position.
For an automatic slider, CSS keyframe animation moves the track at predetermined intervals.
A CSS-only slider is suitable when you need:
JavaScript is usually more appropriate when you need dynamic slides, drag-and-swipe gestures, advanced previous and next controls, autoplay controls, analytics, or more complete accessibility management.
Before writing the code, it helps to understand the main components.
The outer container controls the slider width and hides anything that extends outside its visible area.
overflow: hidden;
Without this property, the other slides may appear beside the active image.
The slide track contains all the individual slides. Flexbox places them in one horizontal row.
display: flex;
Each slide takes up 100% of the slider container’s width.
flex: 0 0 100%;
Every slide has a corresponding radio input. Selecting an input changes the position of the slide track.
The translateX() function moves the track horizontally.
translateX()
transform: translateX(-100%);
A value of -100% displays the second image. A value of -200% displays the third image.
-100%
-200%
Create a folder for the project and organize the files like this:
css-image-slider/ │ ├── index.html ├── styles.css └── images/ ├── mountain-landscape.webp ├── city-night.webp └── beach-sunset.webp
You can use JPG, PNG, WebP, or AVIF images. WebP and AVIF usually provide smaller file sizes than older formats when properly optimized.
Replace the sample image names with your actual filenames.
This first method creates a responsive CSS image slider with navigation dots. Visitors can manually choose which image to display.
Create an index.html file and add the following code:
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>Responsive CSS Image Slider</title> <link rel="stylesheet" href="styles.css"> </head> <body> <main class="page-content"> <section class="image-slider" aria-labelledby="slider-heading" > <h1 id="slider-heading" class="screen-reader-text"> Featured landscape images </h1> <!-- Slider controls --> <input type="radio" name="image-slider" id="slide-control-1" aria-label="Show mountain landscape, slide 1 of 3" checked > <input type="radio" name="image-slider" id="slide-control-2" aria-label="Show city at night, slide 2 of 3" > <input type="radio" name="image-slider" id="slide-control-3" aria-label="Show beach sunset, slide 3 of 3" > <!-- Slide track --> <div class="slides"> <figure class="slide"> <img src="images/mountain-landscape.webp" width="1200" height="675" alt="Snow-covered mountains beneath a clear blue sky" loading="eager" > <figcaption class="slide-caption"> Mountain Landscape </figcaption> </figure> <figure class="slide"> <img src="images/city-night.webp" width="1200" height="675" alt="Illuminated city buildings reflected in a river at night" loading="lazy" > <figcaption class="slide-caption"> City at Night </figcaption> </figure> <figure class="slide"> <img src="images/beach-sunset.webp" width="1200" height="675" alt="Orange sunset above a calm beach and ocean" loading="lazy" > <figcaption class="slide-caption"> Beach Sunset </figcaption> </figure> </div> <!-- Navigation dots --> <div class="slider-navigation" aria-label="Choose an image"> <label for="slide-control-1"> <span class="screen-reader-text"> Show slide 1 </span> </label> <label for="slide-control-2"> <span class="screen-reader-text"> Show slide 2 </span> </label> <label for="slide-control-3"> <span class="screen-reader-text"> Show slide 3 </span> </label> </div> </section> </main> </body> </html>
The .image-slider section contains the complete slider, including its controls, images, captions, and navigation dots.
.image-slider
<section class="image-slider">
The three radio buttons belong to the same group because they share the same name value:
name
name="image-slider"
Only one radio button from the group can be selected at a time.
The first input includes the checked attribute, so the first image appears when the page loads.
checked
The .slides element holds every slide:
.slides
<div class="slides">
CSS will display these slides horizontally.
Each navigation dot is a <label> connected to a radio input through its for attribute.
<label>
for
<label for="slide-control-2">
Selecting this label activates the input with the matching ID:
id="slide-control-2"
The for and id values must match exactly. Otherwise, the slider navigation will not work.
id
Create a styles.css file and add the following code:
styles.css
/* ------------------------------ Basic page styles ------------------------------ */ *, *::before, *::after { box-sizing: border-box; } html { color-scheme: light; } body { margin: 0; min-height: 100vh; font-family: Arial, Helvetica, sans-serif; background: #f4f6fb; color: #172033; } img { max-width: 100%; } .page-content { width: min(100% - 32px, 1100px); margin-inline: auto; padding-block: 64px; } /* ------------------------------ Screen-reader-only content ------------------------------ */ .screen-reader-text { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } /* ------------------------------ Slider container ------------------------------ */ .image-slider { position: relative; width: min(100%, 960px); margin-inline: auto; overflow: hidden; border-radius: 18px; background: #111827; box-shadow: 0 20px 50px rgb(15 23 42 / 18%); isolation: isolate; } /* Keep the radio buttons available to keyboard users while visually hiding them. */ .image-slider > input[type="radio"] { position: absolute; width: 1px; height: 1px; margin: -1px; opacity: 0; } /* ------------------------------ Slide track ------------------------------ */ .slides { display: flex; transition: transform 600ms ease; will-change: transform; } /* ------------------------------ Individual slides ------------------------------ */ .slide { position: relative; flex: 0 0 100%; aspect-ratio: 16 / 9; margin: 0; overflow: hidden; } .slide img { width: 100%; height: 100%; display: block; object-fit: cover; } /* ------------------------------ Image captions ------------------------------ */ .slide-caption { position: absolute; left: 24px; right: 24px; bottom: 24px; width: fit-content; max-width: calc(100% - 48px); padding: 10px 14px; border-radius: 8px; background: rgb(0 0 0 / 68%); color: #ffffff; font-size: clamp(0.875rem, 2vw, 1.125rem); line-height: 1.4; } /* ------------------------------ Move the slide track ------------------------------ */ #slide-control-1:checked ~ .slides { transform: translateX(0); } #slide-control-2:checked ~ .slides { transform: translateX(-100%); } #slide-control-3:checked ~ .slides { transform: translateX(-200%); } /* ------------------------------ Navigation dots ------------------------------ */ .slider-navigation { position: absolute; z-index: 2; left: 50%; bottom: 18px; display: flex; gap: 10px; transform: translateX(-50%); } .slider-navigation label { width: 14px; height: 14px; display: block; border: 2px solid #ffffff; border-radius: 50%; background: rgb(15 23 42 / 50%); cursor: pointer; transition: background-color 200ms ease, transform 200ms ease; } .slider-navigation label:hover { transform: scale(1.2); } /* Show the currently selected dot */ #slide-control-1:checked ~ .slider-navigation label[for="slide-control-1"], #slide-control-2:checked ~ .slider-navigation label[for="slide-control-2"], #slide-control-3:checked ~ .slider-navigation label[for="slide-control-3"] { background: #ffffff; } /* Show keyboard focus around the matching dot */ #slide-control-1:focus-visible ~ .slider-navigation label[for="slide-control-1"], #slide-control-2:focus-visible ~ .slider-navigation label[for="slide-control-2"], #slide-control-3:focus-visible ~ .slider-navigation label[for="slide-control-3"] { outline: 3px solid #38bdf8; outline-offset: 4px; } /* ------------------------------ Responsive layout ------------------------------ */ @media (max-width: 600px) { .page-content { width: min(100% - 20px, 1100px); padding-block: 32px; } .image-slider { border-radius: 12px; } .slide { aspect-ratio: 4 / 3; } .slide-caption { left: 14px; right: 14px; bottom: 48px; max-width: calc(100% - 28px); padding: 8px 10px; } .slider-navigation { bottom: 16px; } .slider-navigation label { width: 16px; height: 16px; } } /* ------------------------------ Reduced-motion preference ------------------------------ */ @media (prefers-reduced-motion: reduce) { .slides, .slider-navigation label { transition: none; } }
Save both files and open index.html in a modern web browser.
The first image should appear automatically. Selecting the second or third navigation dot should move the slider horizontally to the corresponding image.
No JavaScript is required.
The radio inputs appear before the .slides element in the HTML.
That placement is important because the CSS uses the general sibling selector:
#slide-control-2:checked ~ .slides
This selector means:
Find the .slides element that appears after the checked input with the ID slide-control-2.
slide-control-2
The following rule moves the track one complete slider width to the left:
The third slide moves the track two slider widths:
transform: translateX(-200%);
Placing the inputs inside .slides would prevent these selectors from targeting the track correctly.
The basic slider can be customized without changing its core functionality.
Update the maximum width:
.image-slider { width: min(100%, 1200px); }
The slider will grow to a maximum width of 1,200 pixels while remaining responsive on smaller screens.
The example uses an aspect ratio instead of a fixed height:
.slide { aspect-ratio: 16 / 9; }
For a taller slider, use:
aspect-ratio: 4 / 3;
For a square slider, use:
aspect-ratio: 1 / 1;
The aspect-ratio property establishes a preferred relationship between an element’s width and height. Combined with object-fit: cover, it allows images of different proportions to fill a consistent slide area.
aspect-ratio
object-fit: cover
The following rule controls how quickly one image moves to the next:
.slides { transition: transform 600ms ease; }
For a faster transition:
transition: transform 300ms ease;
For a slower transition:
transition: transform 900ms ease;
Delete the <figcaption> elements from the HTML:
<figcaption>
<figcaption class="slide-caption"> Mountain Landscape </figcaption>
You can also keep the captions in the HTML and visually hide them when needed:
.slide-caption { display: none; }
Update the selected-dot background:
background: #ffffff;
For example:
background: #3d5afe;
The slider container already includes:
border-radius: 18px; overflow: hidden;
The overflow rule ensures that the images stay inside the rounded corners.
To add a fourth slide, complete four updates.
<input type="radio" name="image-slider" id="slide-control-4" aria-label="Show forest waterfall, slide 4 of 4" >
<figure class="slide"> <img src="images/forest-waterfall.webp" width="1200" height="675" alt="Waterfall flowing through a green forest" loading="lazy" > <figcaption class="slide-caption"> Forest Waterfall </figcaption> </figure>
<label for="slide-control-4"> <span class="screen-reader-text"> Show slide 4 </span> </label>
#slide-control-4:checked ~ .slides { transform: translateX(-300%); }
Also add the selected-dot and focus styles for the fourth control.
Each additional slide moves the track another 100% to the left:
0
-300%
-400%
You can also create an automatic image slider using CSS keyframe animation.
This version changes images without requiring visitors to select navigation controls.
CSS-only autoplay works best for decorative image sequences. It is less suitable when the slides contain important text, links, forms, or information that users must be able to pause and control.
Use the following HTML:
<section class="automatic-slider" aria-label="Automatic landscape slideshow" tabindex="0" > <div class="automatic-slides"> <figure class="automatic-slide"> <img src="images/mountain-landscape.webp" width="1200" height="675" alt="Snow-covered mountains beneath a clear blue sky" > <figcaption>Mountain Landscape</figcaption> </figure> <figure class="automatic-slide"> <img src="images/city-night.webp" width="1200" height="675" alt="Illuminated city buildings reflected in a river at night" > <figcaption>City at Night</figcaption> </figure> <figure class="automatic-slide"> <img src="images/beach-sunset.webp" width="1200" height="675" alt="Orange sunset above a calm beach and ocean" > <figcaption>Beach Sunset</figcaption> </figure> <!-- Duplicate of the first slide for a smooth loop --> <figure class="automatic-slide" aria-hidden="true"> <img src="images/mountain-landscape.webp" width="1200" height="675" alt="" > </figure> </div> </section>
The first image is repeated at the end. This allows the animation to move from the third image to a duplicate of the first before resetting.
Add the following CSS:
.automatic-slider { position: relative; width: min(100%, 960px); margin-inline: auto; overflow: hidden; border-radius: 18px; background: #111827; box-shadow: 0 20px 50px rgb(15 23 42 / 18%); } .automatic-slider:focus-visible { outline: 4px solid #38bdf8; outline-offset: 5px; } .automatic-slides { display: flex; animation: automatic-slide 15s infinite ease-in-out; } .automatic-slide { position: relative; flex: 0 0 100%; aspect-ratio: 16 / 9; margin: 0; overflow: hidden; } .automatic-slide img { width: 100%; height: 100%; display: block; object-fit: cover; } .automatic-slide figcaption { position: absolute; left: 24px; bottom: 24px; padding: 10px 14px; border-radius: 8px; background: rgb(0 0 0 / 68%); color: #ffffff; font-size: 1rem; } /* Pause while the slider is hovered or focused */ .automatic-slider:hover .automatic-slides, .automatic-slider:focus-within .automatic-slides { animation-play-state: paused; } /* Automatic movement */ @keyframes automatic-slide { 0%, 26% { transform: translateX(0); } 33%, 59% { transform: translateX(-100%); } 66%, 92% { transform: translateX(-200%); } 100% { transform: translateX(-300%); } } /* Disable automatic movement when reduced motion is requested */ @media (prefers-reduced-motion: reduce) { .automatic-slides { animation: none; transform: none; } .automatic-slide { display: none; } .automatic-slide:first-child { display: block; } }
The animation lasts 15 seconds:
animation: automatic-slide 15s infinite ease-in-out;
The infinite value restarts it continuously.
infinite
The keyframes hold each image in place before moving to the next one:
0%, 26% { transform: translateX(0); }
The second image appears after the track moves left by 100%:
The third image appears at -200%.
The duplicate of the first image appears at -300%, creating a smoother visual loop when the animation resets.
Change the animation duration:
For faster autoplay:
animation: automatic-slide 9s infinite ease-in-out;
For slower autoplay:
animation: automatic-slide 24s infinite ease-in-out;
Do not make slides move so quickly that visitors cannot understand their contents.
For most content-focused websites, the manual version is the safer choice because visitors control when the slide changes.
The slider already uses several responsive techniques.
width: min(100%, 960px);
This allows the slider to fill its available container while preventing it from becoming wider than 960 pixels.
aspect-ratio: 16 / 9;
This keeps the slider’s proportions consistent across different screen sizes.
object-fit
object-fit: cover;
This makes every image fill the available slide area. Parts of a particularly tall or wide image may be cropped.
Use contain when showing the entire image is more important than filling the complete slider area:
contain
object-fit: contain;
This may create empty space around some images.
Avoid code such as:
width: 960px;
A fixed width can make the slider overflow on smaller screens.
Use percentages, max-width, or the min() function instead.
max-width
min()
Touch controls should be easy to select. The example increases the navigation-dot size on smaller screens:
@media (max-width: 600px) { .slider-navigation label { width: 16px; height: 16px; } }
Changing the slide track to this on mobile will stop the carousel behavior:
.slides { flex-direction: column; }
The slider depends on the images remaining in a horizontal row. Keep the default Flexbox direction and adjust the container dimensions instead.
Large, unoptimized slider images can affect loading speed, especially when several images load on the same page.
Do not upload a 4,000-pixel image when the slider displays it at a maximum width of 960 pixels.
Prepare images close to the largest size at which they will be displayed.
For high-density screens, you may provide a larger version through srcset.
srcset
WebP and AVIF can often reduce file size while maintaining good visual quality.
Keep a compatible fallback when your audience or publishing system requires one.
Avoid filenames such as:
image1.jpg photo-final-new.jpg slider-img-3.png
Use descriptive filenames:
snow-covered-mountain-slider.webp city-skyline-night-carousel.webp orange-beach-sunset-slide.webp
Avoid generic alt text:
alt="Image 1"
Describe the image’s relevant content:
alt="Snow-covered mountains beneath a clear blue sky"
Google recommends descriptive alt text because it provides image context and supports people who cannot see the image. It also recommends responsive image techniques and standard image elements for image discovery.
Do not fill alt text with unrelated keywords. Describe the actual image and its purpose on the page.
Include width and height attributes:
width
height
<img src="images/mountain-landscape.webp" width="1200" height="675" alt="Snow-covered mountains beneath a clear blue sky" >
This gives the browser information about the image proportions before the file finishes loading.
For more control over image loading, use srcset and sizes:
sizes
<img src="images/mountain-landscape-1200.webp" srcset=" images/mountain-landscape-480.webp 480w, images/mountain-landscape-800.webp 800w, images/mountain-landscape-1200.webp 1200w " sizes=" (max-width: 600px) calc(100vw - 20px), 960px " width="1200" height="675" alt="Snow-covered mountains beneath a clear blue sky" >
The browser can select an appropriate image file based on the screen and layout.
A CSS-only slider can support basic keyboard interaction, but it has limitations.
In the manual example, the radio buttons remain available in the keyboard navigation order. A keyboard user can focus the radio group and use arrow keys to change the selection.
Avoid hiding the inputs using:
display: none;
That removes them from the keyboard interaction flow in common browser behavior.
The example visually hides the controls while keeping them present:
opacity: 0; width: 1px; height: 1px;
Every informative image should include accurate alt text. Decorative images can use an empty alt attribute:
alt=""
The example adds a focus outline to the navigation dot connected to the currently focused input.
Never remove focus outlines unless you replace them with another clear focus indicator.
Use:
@media (prefers-reduced-motion: reduce) { .slides { transition: none; } }
The prefers-reduced-motion media feature detects when a person has requested fewer nonessential animations through their device settings.
prefers-reduced-motion
Automatically rotating carousels can be difficult for visitors who need more time to read or interact with content.
W3C carousel guidance recommends giving users control over automatic movement, stopping rotation when keyboard focus enters the carousel, pausing it on hover, and providing a way to stop and restart it. A CSS-only autoplay slider cannot provide all of this behavior as reliably as a scripted, accessibility-focused component.
Use the automatic CSS example for decorative images rather than essential information.
Choose a JavaScript-based accessible carousel when you need:
Possible cause: The container does not hide overflowing slides.
Add:
.image-slider { overflow: hidden; }
Also confirm that the slides are arranged horizontally:
.slides { display: flex; }
Confirm that each label’s for value matches its input ID.
Correct:
<input id="slide-control-2" type="radio" name="image-slider"> <label for="slide-control-2"></label>
Incorrect:
<input id="slide-2" type="radio" name="image-slider"> <label for="slide-control-2"></label>
The values do not match in the incorrect example.
The input must appear before the element targeted by the sibling selector.
Correct structure:
<input type="radio" id="slide-control-1"> <div class="slides"> <!-- Slides --> </div>
This selector can now work:
#slide-control-1:checked ~ .slides { transform: translateX(0); }
If the input is placed inside .slides, it cannot select the parent track using a sibling selector.
Give every slide the same aspect ratio:
Then make the images fill that area:
.slide img { width: 100%; height: 100%; object-fit: cover; }
Cropping is caused by:
Change it to:
This displays the entire image but may leave empty space around it.
Check for fixed widths:
Replace them with responsive values:
Also ensure the page container has reasonable horizontal spacing.
Check its transform value.
For three full-width slides:
#slide-control-1:checked ~ .slides { transform: translateX(0); } #slide-control-2:checked ~ .slides { transform: translateX(-100%); } #slide-control-3:checked ~ .slides { transform: translateX(-200%); }
A looping animation resets from its final position to its initial position.
To make that reset less noticeable, duplicate the first image at the end of the track. Move to the duplicate during the final keyframe, then let the animation reset to the original first image.
Because the two images are visually identical, the reset is not noticeable.
.automatic-slider:hover .automatic-slides, .automatic-slider:focus-within .automatic-slides { animation-play-state: paused; }
Remember that hover is not available in the same way on every touchscreen device.
Give the navigation container a higher stacking level:
.slider-navigation { z-index: 2; }
The slider container should also use:
position: relative;
Use a sufficiently opaque background behind the text:
.slide-caption { background: rgb(0 0 0 / 68%); color: #ffffff; }
Avoid placing unprotected text directly over detailed or high-contrast images.
Both approaches have valid uses.
Use CSS only when your requirements are simple and fixed. Use JavaScript when the slider is an important interactive component rather than a basic visual effect.
Follow these guidelines when adding a slider to a real website:
Creating a responsive image slider in HTML and CSS without JavaScript is possible with radio buttons, labels, Flexbox, CSS transforms, and keyframe animations.
The manual version is ideal for portfolios, product images, travel photos, galleries, and other situations where visitors should control the active image. The automatic version can work for decorative slideshows, but it should not be used for essential information unless users have complete control over the movement.
Start with the manual slider source code, replace the sample images, update the captions and alt text, and adjust the colors to match your website.
For more advanced requirements such as swipe navigation, dynamic slides, previous and next buttons, autoplay controls, and screen-reader announcements, use an accessibility-focused JavaScript solution.
For WordPress projects that need an interactive before-and-after comparison rather than a standard carousel, Code Canel’s WP Before After Image Slider provides a dedicated no-code option.
Yes. Radio inputs, labels, Flexbox, sibling selectors, and CSS transforms can create a manual image slider without JavaScript. CSS keyframes can also create automatic slide movement.
Place the images inside a horizontal Flexbox track and apply a repeating @keyframes animation to that track. At different animation percentages, use translateX() to display the next image.
Use a fluid width such as width: 100%, set a reasonable max-width, give the slides an aspect-ratio, and apply object-fit: cover to the images. Avoid fixed widths that are larger than mobile screens.
width: 100%
Add another radio input, slide element, navigation label, and checked-state transform rule. Every new slide moves the track another 100% to the left.
Yes, but the HTML becomes more complicated. You must display different labels for the previous and next destination based on the currently selected radio input. Navigation dots are simpler and easier to maintain in a basic CSS-only tutorial.
The most common causes are mismatched label and input values, incorrect HTML order, missing overflow: hidden, incorrect transform percentages, or slides that do not use a full-width flex basis.
overflow: hidden
A simple manual slider can provide basic keyboard support when its radio inputs remain focusable. However, CSS alone cannot reliably manage every accessibility requirement of an advanced carousel, especially automatic rotation, changing announcements, dynamic slide visibility, and focus control.
This page was last edited on 23 July 2026, at 6:15 pm
Your email address will not be published. Required fields are marked *
Comment *
Name *
Email *
Website
Save my name, email, and website in this browser for the next time I comment.
How many people work in your company?Less than 1010-5050-250250+
By proceeding, you agree to our Privacy Policy