A lot of sliders load a script just to change which image is showing. CSS can handle that part on its own.
Hero banners, product galleries, and testimonial rows use the pattern mostly because there’s no script to load or keep updated.
Autoplay is where it gets less simple. WCAG 2.2 (W3C, 2023) success criterion 2.2.2, Level A, requires a pause, stop, or hide mechanism for moving content that starts automatically, lasts more than five seconds, and sits beside other content.
What Is a CSS Slideshow?
See the Pen
CSS-only slideshow by Johan Lagerqvist (@lgrqvst)
on CodePen.
The names vary. Developers say image slider, or CSS carousel, and they all point at the same pattern. Keyframes, scroll snapping, or state selectors do the switching between slides, and no JavaScript is involved in the core behavior.
Underneath, it’s a box with a fixed width, an aspect-ratio, and overflow hidden, so only one slide shows at a time. The slides are images or content blocks. For fades they stack in one CSS grid cell, and for sliding they sit in a row with flexbox.
Something still has to decide which slide is visible. That can be a keyframes animation, a checked radio input, a :target anchor, or just the visitor scrolling.
CSS alone can produce autoplay, click navigation, touch swipe, and position indicators, but no single technique gets all four without giving something up.
A script that adds an “active” class on a timer is a JavaScript slideshow, even when CSS performs the fade.
A JavaScript carousel adds slide state, events, and play and pause controls, at the cost of a script to load and maintain. A CSS slideshow gives up those features and ships no script at all.
Which CSS Slideshow Method Fits Your Project?
See the Pen
SlideShow Animated by Manu K (@manukn)
on CodePen.
Keyframes suit autoplay banners, and radio buttons suit small click-through galleries. Scroll snap is the one for swiping on touch screens, and the native scroll-marker carousel just adds buttons and dots on top of it.
| Method | Gives you | Does not give you | Best for |
|---|---|---|---|
| Keyframes | Autoplay, looping, pause on hover | Click navigation, swipe | Hero banners, background rotators |
| Radio buttons | Click navigation, dot indicators | Autoplay, swipe | Small click-through galleries |
| Scroll snap | Touch swipe, trackpad scroll, snapping | Autoplay, indicators | Mobile galleries |
| Native carousel | Scroll buttons, dot markers, snapping | Autoplay | Enhancing a scroll snap base |
The biggest gap between the last two rows is cross-browser compatibility. Scroll snap has been around for years. ::scroll-button() and ::scroll-marker only first shipped in Chrome 135 and Edge 135 in April 2025, according to Chrome for Developers and the web-platform-dx tracker.
Tutorials don’t agree about keyframes. Code Boxx steers readers away from keyframe autoplay and calls it more complicated than JavaScript. CSS Play’s April 2026 slideshow goes the other way, building autoplay from keyframes and exposing image count and timings as CSS variables.
Both positions hold up, depending on the job. A fixed set of slides stays simple. Once the slide count changes often, the effort grows.
For a static homepage banner, keyframes is the lowest-maintenance pick, and it’s the one I’d reach for.
How to Build an Autoplay CSS Slideshow With Keyframes
See the Pen
Auto/Manual Image Slideshow by Matt Fried (@mattfried)
on CodePen.
The loop runs because animation-iteration-count is set to infinite. Every slide sits in the same grid cell, every slide gets the same fade keyframes, and each slide’s animation-delay is offset by its share of the cycle.
A refresher on keyframe rules covers the percentage syntax used below.
- Wrap the slides in a container with a fixed aspect-ratio and overflow: hidden.
- Place every slide in the same grid cell (grid-area: 1 / 1) and set opacity: 0 on each.
- Set the cycle length to the slide count times the seconds per slide (5 slides at 5 seconds gives 25 seconds).
- Write one @keyframes rule that fades a slide in, holds it, and fades it out.
- Apply that animation to every slide with animation-duration set to the full cycle, animation-iteration-count set to infinite, and animation-fill-mode set to backwards so delayed slides sit at their 0% keyframe before they start.
- Give each slide an animation-delay of its index times 5 seconds (0s, 5s, 10s, 15s, 20s).
Calculating keyframe percentages and delays
Take 5 slides at 5 seconds each. The cycle is 25 seconds, and each slide owns 20% of it.
- At 0% the opacity is 0 and the slide starts hidden
- At 4% it reaches opacity 1, a 1-second fade-in (1 of 25 seconds)
- At 20% the opacity is still 1, so the slide holds until second 5
- At 24% it drops back to opacity 0, a 1-second fade-out that overlaps the next slide’s fade-in
- At 100% it stays at 0 and waits for the next cycle
Each slide stays fully visible for 4 seconds (4% to 20%), and the overlap produces a 1-second crossfade.
Real demos run slower. CSS Play’s April 2026 slideshow uses 7 images at 1.5 seconds of movement and 4.5 seconds of stillness each, a 42-second cycle.
One flaw shows up on page load. Slide one starts at opacity 0, so the container is empty for the first second. A separate keyframe rule for slide one that starts at opacity 1 removes the gap.
Fade versus horizontal slide
See the Pen
Fading Slideshow by Cliff Pyles (@cliffpyles)
on CodePen.
Fades animate opacity on slides stacked in one grid cell, which means no slide width math. The keyframes move opacity from 0 to 1 and back, and the percentages need recalculating whenever the slide count changes.
Sliding works differently. The slides sit in a flex row inside a track, and translateX moves the track. Hold keyframes repeat the same value for 16% of the cycle, then the track moves for 4% (4 seconds still, 1 second moving, in a 25-second cycle). Repeating the first slide at the end of the row keeps the loop moving forward instead of rewinding.
A zoom effect is just a fade with a slow transform from scale(1) to scale(1.08) across the hold. Put easing inside the keyframes, because animation-timing-function applies to the segment that starts at each keyframe.
Pausing on hover
animation-play-state has been Baseline widely available since September 2015 (MDN Web Docs), and a paused animation resumes where it stopped instead of restarting.
Setting animation-play-state: paused on .slideshow:hover .slide and .slideshow:focus-within .slide freezes the slideshow for mouse and keyboard users.
It has limits. A pause mid-fade leaves two half-transparent slides on screen, and touch screens have no hover at all, so treat this as a convenience and not an accessibility feature.
How to Build a CSS Slideshow With Radio Buttons and Labels
The browser keeps track of which radio is checked, so no script is needed. One radio input per slide goes before the slides, labels act as the clickable dots, and :checked plus a sibling combinator shows the slide that follows the checked input.
Source order decides whether this works, because a sibling selector only reaches elements that come after the input.
The catch is that there’s no autoplay and no touch swipe, and every slide you add means another input and another label.
Radio inputs and the sibling selector
All the radio inputs come first, sharing one name attribute, with the first one checked. The labels or dots go next, and the slide track goes last.
The :checked pseudo-class has been Baseline widely available since July 2015 (MDN Web Docs), so the pattern runs in any current browser. A rule like #s2:checked ~ .track { transform: translateX(-100%); } moves the track to the second slide.
Hide the inputs visually instead of using display: none. A display: none input leaves the tab order, and keyboard users lose the controls.
Dot and arrow controls
See the Pen
Slideshow in a button. by Martyn Knapton (@designmechanic)
on CodePen.
For dots, a label with for=”s3″ checks the third radio when clicked, and a rule such as #s3:checked ~ .dots label[for=”s3″] styles the active dot.
Arrows take more work. Each slide needs two labels, one pointing to the previous radio and one to the next, shown only on the active slide. The markup grows with every slide you add.
:target and :has() variants
:target swaps the radio inputs for anchor links. Each slide has an id and the dots are links such as href=”#slide2″. Each click adds a history entry and scrolls the browser to that slide, so the Back button steps through slides and the page can jump.
:has() lets the container react to the checked input. A rule like .slideshow:has(#s2:checked) styles the parent from the checked state, so inputs can sit anywhere inside the container. :has() has worked in all major browsers since December 2023 and is now Baseline widely available (MDN Web Docs).
Anchor :has() to the slideshow container, not body or :root. MDN warns that broad anchors force the browser to re-check the condition after every DOM change.
How to Build a Swipeable Slideshow With CSS Scroll Snap
See the Pen
Pure CSS Slideshow Gallery by Roko C. Buljan (@rokobuljan)
on CodePen.
Set scroll-snap-type: x mandatory on a horizontally scrolling container and scroll-snap-align: center on each slide. Swipe, trackpad scrolling, and snapping to the nearest slide are then handled by the browser.
Scroll snap is defined in the CSS Scroll Snap Module Level 1, and scroll-snap-type has been Baseline widely available since April 2022 (MDN Web Docs).
The container needs display: flex, overflow-x: auto, and scroll-snap-type: x mandatory. Each slide gets flex: 0 0 100% and scroll-snap-align: center. Adding scroll-snap-stop: always keeps a hard flick from jumping past a slide.
Use mandatory, not proximity. With proximity the browser decides whether to snap (MDN lists it as the default strictness), so a slide can come to rest between two positions.
Dots and arrows can be plain anchor links such as href=”#slide3″, which scroll the container to the target slide. Adding scroll-behavior: smooth animates the jump.
Scroll snap reacts only to scrolling, so autoplay is outside its scope. Built-in indicators and arrow buttons come from the native carousel pseudo-elements covered in the next section.
Does the Native CSS Carousel Replace the Older Methods?
Not yet. The native carousel gives a scroll snap row its buttons and dot markers without JavaScript, but it adds no autoplay and is still limited to Chromium browsers in the web-platform-dx tracker as of October 2026.
Both features come from the CSS Overflow Module Level 5. Scroll snap stays the base layer, and the markers and buttons sit on top of it.
Scroll buttons and scroll markers
::scroll-button(left) and ::scroll-button(right) attach to the scroll container and generate browser-managed buttons. Each press scrolls 85% of the scroll area (Chrome for Developers, 2025).
The content property takes a visible glyph and, after a slash, a text alternative for screen readers.
Markers work from the other direction. Setting scroll-marker-group: after on the container, plus ::scroll-marker on each slide, generates one dot per slide, and the :target-current state styles the dot for the slide in view.
A newer property, scroll-target-group, turns a list of anchor links into scroll markers, so the dots can live in your own HTML.
Wrap all of it in @supports (scroll-marker-group: after). Browsers without support then keep a plain scroll snap carousel.
Linking slide effects to scroll position
Scroll-driven animations tie an animation’s progress to scroll position instead of time.
animation-timeline: scroll() follows a container’s scroll offset. animation-timeline: view() follows an element’s position inside its scroller, and animation-range picks which part of that travel drives the keyframes.
On a scroll snap row, a view() timeline can fade or scale each slide as it enters and leaves the frame. The slides animate with the swipe instead of a clock.
Chrome 115, Edge 115, and Safari 26 ship the feature, and Firefox keeps it behind layout.css.scroll-driven-animations.enabled, according to MDN’s browser-compat-data. Guard the rules with @supports (animation-timeline: scroll()) so slides stay fully visible everywhere else.
Adoption of the markers is still tiny, about 0.121% of page loads, against about 21.1% for scroll snap (Chrome Platform Status, via web-platform-dx, October 2026). Firefox and Safari list neither ::scroll-marker (web-platform-dx tracker) nor ::scroll-button() (Chrome for Developers support table), as of October 2026.
Some 2026 tutorials report Safari support for the markers. The trackers do not, so check Can I use for the current state before launch.
How to Make a CSS Slideshow Responsive
See the Pen
ScrollMagic Tutorial – Fullscreen Slideshow by Petr Tichy (@ihatetomatoes)
on CodePen.
Give the container a fixed aspect-ratio and a fluid width, then let the images fill it with object-fit: cover. The container never resizes when an image arrives.
A slideshow follows the same responsive design rules as the rest of the page: fluid widths, ratios instead of pixel heights, and breakpoints only where the content needs them.
Sizing slides and images
On the container, use width: 100%, a max-width, and one aspect ratio for every slide (16 / 9 for banners, 3 / 2 for photos).
CSS Play’s April 2026 slideshow caps the container at 750px, sets max-width to 95%, and reads its ratio from a custom property (3 / 2).
Images get width: 100%, height: 100%, and object-fit: cover. object-position shifts the crop toward the subject when a portrait photo sits in a landscape frame.
Sizing the track in cqw units, as the same demo does, scales slides with the container instead of the screen. Viewport units size the slideshow to the screen for a full-screen version, but a mobile browser toolbar can make 100vh taller than the visible area, so test on a real phone.
Missing image dimensions are among the layout shift causes web.dev lists, and a good Cumulative Layout Shift score is 0.1 or less (web.dev, Optimize Cumulative Layout Shift). Its fix is width and height attributes on every image, or a reserved space through aspect-ratio.
Lazy loading slide images
Lazy loading does nothing for a stacked fade slideshow, though a scroll snap row does benefit from it.
Browsers load a lazy image once it comes within a threshold distance of the viewport. Stacked slides all sit inside the viewport, so every one of them loads at once, opacity 0 or not.
In a scroll snap row, slides beyond that threshold wait, which cuts the weight of the first page load.
- Leave loading=”lazy” off the first slide. In a hero banner it is usually the Largest Contentful Paint element, and Lighthouse flags a lazily loaded one
- Add loading=”lazy” to later slides only in a scroll snap row
- Keep width and height on every img, even inside an aspect-ratio container
How to Make a CSS Slideshow Accessible
Start with prefers-reduced-motion. Autoplay also needs a pause control, and hidden slides shouldn’t stay in the tab order. WCAG 2.2 success criterion 2.2.2 (Level A) requires a pause, stop, or hide mechanism for moving content that starts automatically, lasts more than five seconds, and sits beside other content (W3C).
Inside @media (prefers-reduced-motion: reduce), set animation: none on the slides and show the first one, or swap the layout for a scroll snap row the visitor controls.
For the pause control, place a checkbox before the slideshow and label it “Pause slideshow”, then add #pause:checked ~ .slideshow .slide { animation-play-state: paused; }. A native checkbox reports its state to screen readers without any script.
Slides at opacity 0 stay focusable, so links inside hidden slides still take focus. Animating visibility alongside opacity removes them. It’s an easy thing to miss until you tab through the page.
The W3C carousel pattern also stops rotation when keyboard focus enters the carousel, and it asks for a visible rotation control.
The roles and labels come from ARIA. That pattern sets aria-roledescription=”carousel” on the container, aria-roledescription=”slide” on each slide with a label such as “2 of 6”, and aria-live off while rotation runs.
Plain HTML carries those attributes. CSS cannot switch aria-live to polite when the visitor stops the rotation, which the pattern requires, so a CSS slideshow with autoplay stays short of the full pattern.
Native markers cover part of the gap. Chrome for Developers says keyboard navigation on markers behaves like a focusgroup and screen readers report them like a tablist, which matches the tabbed carousel variant in the W3C pattern. Radio-button dots expose a form control instead.
Caption text over photos needs enough color contrast, and a solid or semi-transparent panel behind the text is more reliable than a shadow. Every image still needs alt text that describes the slide.
Check the result in the Chrome DevTools accessibility tree, run a Lighthouse accessibility audit, and tab through the page once with the reduced motion setting on.
When a CSS Slideshow Is the Wrong Choice
See the Pen
Responsive Vanilla JS Slideshow by Arden (@aderaaij)
on CodePen.
Skip it when slides come from a CMS, or when you need slide-change events, analytics, or the full W3C pattern with live-region updates. Those jobs call for JavaScript.
A static banner of 3 slides needs no library. A product carousel that editors change weekly turns CSS-only into a maintenance chore.
On the plus side, there’s no script to load, update, or debug, and it still works with JavaScript disabled. Editors can read the plain HTML and CSS, which counts for more than it sounds.
The downsides are real, though. There are no slide-change events, so analytics cannot see which slide a visitor viewed, and no live-region updates when rotation stops. Scroll snap rows stop at the last slide instead of looping, and desktop users get no click-and-drag with a mouse.
Swiper’s own comparison page puts Splide at about 12 KB gzipped and Swiper at about 25 to 45 KB depending on modules. The same page describes Splide as built with ARIA support from the start and Swiper’s accessibility features as an opt-in module.
A site already on Bootstrap can use its Bootstrap carousel, which the docs describe as built with CSS 3D transforms and a bit of JavaScript. Those docs recommend avoiding autoplaying carousels for accessibility reasons and tie the animation to prefers-reduced-motion.
Which library to pick depends on the downside that hurts most. Events and analytics point to Swiper, and ARIA by default points to Splide. If the stack is already Bootstrap, its own carousel is the obvious choice.
CSS Slideshow examples
Watch Out, It’s an Owl!
See the Pen
Slideshow Owl Carousel + YouTube by pierrinho (@pierrinho)
on CodePen.
Slide but Make it Classy
See the Pen
Slick Slideshow with blur effect by Fabio Ottaviani (@supah)
on CodePen.
First Impressions Matter
See the Pen
Mobile First Product Slideshow Widget by Kevin Lesht (@klesht)
on CodePen.
Immerse in Nature with Every Slide
See the Pen
CSS3 Fullscreen Background Slideshow by Chris (@leetech)
on CodePen.
Slide Peek-a-Boo
See the Pen
Slideshow CSS with img preview by Christophe Molina B. (@10tribu)
on CodePen.
Shadowplay Slide
See the Pen
Silhouette zoom slideshow by Mikael Ainalem (@ainalem)
on CodePen.
Bubbles ‘n’ Slides
See the Pen
Bubble Slideshow Component by Erik Jung (@erikjung)
on CodePen.
Slide, Burger, Repeat
See the Pen
Fullscreen slideshow with menu by Hitz Kareaga (@hitzkareaga)
on CodePen.
Apple Goodness on Display
See the Pen
Apple devices slide show by Comehope (@comehope)
on CodePen.
Parallax Vibes with a Twist’
See the Pen
Slideshow Parallax with TweenMax by Bruno Carvalho (@bcarvalho)
on CodePen.
Tabs Are the New Cool
See the Pen
Hero Slideshow by André Villela (@dedevillela)
on CodePen.
Seeing Double with Dual Panes
See the Pen
Dual Slideshow Demo by Jacob Davidson (@Reklino)
on CodePen.
It’s a Split Decision
See the Pen
Split-screen Slideshow by Sean Free (@seanfree)
on CodePen.
Distort, but Make It Artsy
See the Pen
Distorted Gallery by Nikolay Talanov (@suez)
on CodePen.
Nautilus Slideshow
See the Pen
Nautilus Slideshow by Nikolas Payne (@npayne)
on CodePen.
Geometrical Birds – Slideshow
See the Pen
Geometrical Birds – slideshow by Mikael Ainalem (@ainalem)
on CodePen.
Cycle Slideshow Slider
See the Pen
Cycle Slideshow Slider by Bram de Haan (@atelierbram)
on CodePen.
Untitled Slider
See the Pen
Untitled Slider by Nathan Taylor (@nathantaylor)
on CodePen.
Tweenmax Slideshow
See the Pen
Tweenmax slideshow by Matheus Verissimo (@astrixsz)
on CodePen.
CSS Slideshow FAQ
Can a CSS slideshow include captions and thumbnails?
Captions are easy. A figcaption inside each slide fades or moves with it. Thumbnails are small images placed inside the labels of the radio button method, or inside anchor links in a scroll snap row.
Thumbnails need one control per slide, so the markup grows with the slide count.
How can a CSS slideshow run vertically?
Swap the horizontal axis for the vertical one. Keyframes animate translateY on a column track, and scroll snap uses scroll-snap-type: y mandatory on a container with a fixed height and overflow-y: auto.
Each slide then takes the container’s height instead of its width.
How many slides can a keyframes slideshow hold?
There’s no fixed limit, but the cycle grows with every slide. At 5 seconds per slide, 12 slides make a 60-second cycle and give each slide 8.33% of the keyframes, so the percentages stop being round numbers.
Past a dozen slides, a scroll snap row is easier to maintain.
Are images in a CSS slideshow indexed by Google?
When they are img elements, yes. Google Search Central says it finds images in the src of img elements and does not index CSS images, so slides set as background-image stay out of Image Search.
Keep content images in img tags with descriptive alt text.
Is a 3D effect possible in a CSS slideshow?
See the Pen
Slideshow 3d showcase by Jerome Renders (@JeromeRenders)
on CodePen.
It is. Set perspective on the container, give the slide track transform-style: preserve-3d, and turn it with rotateY or rotateX. Keyframes or checked radio inputs trigger the rotation.
Bootstrap’s carousel is also built with CSS 3D transforms, according to its documentation.
What to Check Before Publishing a CSS Slideshow
Check reduced motion first, then keyboard use, then a slow connection, and browser support last. That order is deliberate.
The two accessibility checks lead because a failure there can force markup changes. Connection speed and browser gaps need only adjusted values and fallbacks.
Browser support is the check most likely to expire.
If Firefox and Safari ship ::scroll-button() and ::scroll-marker(), the native carousel can replace radio buttons for click navigation, and the browser check shrinks to older versions.
Chrome for Developers still lists both features as unsupported in Firefox and Safari (checked October 2026).
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026


