A “slider”, as in, a bunch of boxes set in a row that you can navigate between. You know what a slider is. There are loads of features you may want in a slider. Just as one example, you might want the slider to be swiped or scrolled. Or, you might not want that, and to have the slider only respond to click or tappable buttons that navigate to slides. Or you might want both. Or you might want to combine all that with autoplay.
I’m gonna go ahead and say that sliders are complicated enough of a UI component that it’s use JavaScript territory. Flickity being a fine example. I’d also say that you can get pretty far with a nice looking functional slider with HTML and CSS alone. Starting that way makes the JavaScript easier and, dare I say, a decent example of progressive enhancement.
Let’s consider the semantic markup first.
A bunch of boxes is probably as simple as:
<div class="slider">
<div class="slide" id="slide-1"></div>
<div class="slide" id="slide-2"></div>
<div class="slide" id="slide-3"></div>
<div class="slide" id="slide-4"></div>
<div class="slide" id="slide-5"></div>
</div>
With a handful of lines of CSS, we can set them next to each other and let them scroll.
.slider {
width: 300px;
height: 300px;
display: flex;
overflow-x: auto;
}
.slide {
width: 300px;
flex-shrink: 0;
height: 100%;
}

Might as well make it swipe smoothly on WebKit based mobile browsers.
.slider {
...
-webkit-overflow-scrolling: touch;
}

We can do even better!
Let’s have each slide snap into place with snap-points.
This has changed a little bit since the original posting! I’m updating the code blocks below.
.slider {
...
/* CURRENT way. */
scroll-snap-type: x mandatory;
/* OLD WAY. Probably not worth including at all.
-webkit-scroll-snap-points-x: repeat(300px);
-ms-scroll-snap-points-x: repeat(300px);
scroll-snap-points-x: repeat(300px);
-webkit-scroll-snap-type: mandatory;
-ms-scroll-snap-type: mandatory;
scroll-snap-type: mandatory;
*/
}
.slides > div {
/* CURRENT way. */
scroll-snap-align: start;
}
Look how much nicer it is now:

Jump links
A slider probably has a little UI to jump to a specific slide, so let’s do that semantically as well, with anchor links that jump to the correct slide:
<div class="slide-wrap">
<a href="#slide-1">1</a>
<a href="#slide-2">2</a>
<a href="#slide-3">3</a>
<a href="#slide-4">4</a>
<a href="#slide-5">5</a>
<div class="slider">
<div class="slide" id="slide-1">1</div>
<div class="slide" id="slide-2">2</div>
<div class="slide" id="slide-3">3</div>
<div class="slide" id="slide-4">4</div>
<div class="slide" id="slide-5">5</div>
</div>
</div>
Anchor links that actually behave as a link to related content and semantic and accessible so no problems there (feel free to correct me if I’m wrong).
Let’s style thing up a little bit… and we got some buttons that do their job:

On both desktop and mobile, we can still make sure we get smooth sliding action, too!
.slides {
...
scroll-behavior: smooth;
}
Maybe we’d only display the buttons in situations without nice snappy swiping?
If the browser supports scroll-snap-type
, it’s got nice snappy swiping. We could just hide the buttons if we wanted to:
@supports (scroll-snap-type) {
.slider > a {
display: none;
}
}
Need to do something special to the “active” slide?
We could use :target
for that. When one of the buttons to navigate slides is clicked, the URL changes to that #hash, and that’s when :target
takes effect. So:
.slides > div:target {
transform: scale(0.8);
}

There is a way to build this slide with the checkbox hack as well, and still to “active slide” stuff with :checked
, but you might argue that’s a bit less semantic and accessible.
Here’s where we are so far.
See the Pen
Real Simple Slider by Chris Coyier (@chriscoyier)
on CodePen.
This is where things break down a little bit.
Using :target
is a neat trick, but it doesn’t work, for example, when the page loads without a hash. Or if the user scrolls or flicks on their own without using the buttons. I both don’t think there is any way around this with just HTML and CSS, nor do I think that’s entirely a failure of HTML and CSS. It’s just the kind of thing JavaScript is for.
JavaScript can figure out what the active slide is. JavaScript can set the active slide. Probably worth looking into the Intersection Observer API.
What are more limitations?
We’ve about tapped out what HTML and CSS alone can do here.
- Want to be able to flick with a mouse? That’s not a mouse behavior, so you’ll need to do all that with DOM events. Any kind of exotic interactive behavior (e.g. physics) will require JavaScript. Although there is a weird trick for flipping vertical scrolling for horizontal.
- Want to know when a slide is changed? Like a callback? That’s JavaScript territory.
- Need autoplay? You might be able to do something rudimentary with a checkbox,
:checked
, and controlling theanimation-play-state
of a@keyframes
animation, but it will feel limited and janky. - Want to have it infinitely scroll in one direction, repeating as needed? That’s going to require cloning and moving stuff around in the DOM. Or perhaps some gross misuse of
<marquee>
.
I’ll leave you with those. My point is only that there is a lot you can do before you need JavaScript. Starting with that strong of a base might be a way to go that provides a happy fallback, regardless of what you do on top of it.
This is a great article! Sadly I needed it 3 years ago. Sliders are a big part of mobile design, or at least can be, for me. I love to reduce the size of my sites/apps wherever I can.
Another interesting effect that can be done in pure CSS:
show all the slides in a mini mosaic.
Ex: http://xem.github.io/talks/boxmodel/index.html#slide0
(click the bottom button)
Excellent samples
FYI unfortunately CSS Scroll Snap Points isn’t jet supported on Chrome,
https://bugs.chromium.org/p/chromium/issues/detail?id=497851
See, now you’re just making me feel ignorant, Chris! I had no idea a number of those CSS attributes existed! :p
Nice simple implementation!
One thing that bothers me… If you get rid of the transition on
.slides > div
, the positioning of the slides when we use the buttons is wrong. I just can’t figure out why…Look here, the only change is commenting the transition line (:42).
It looks like the initial positioning is fine to me. All I can think of is that you were expecting it to automatically set itself to fill the whole div, which was what the transitioning was for – to smoothly make the slide grow to fill the div. Instead, it jumps, since there’s no easing involved anymore.
That was a great article! It just proves how powerful and underestimated CSS is nowadays. It pains me to see developers go straight to JavaScript when they have a problem. I wish more articles were written about how to take a CSS approach rather than a JavaScript approach.
I feel as if I am one of the few website developers on this planet who dislikes JavaScript. It always seems to slow down a webpage in some shape and form. Maybe the reason I find this so hurtful is because I have a game development background, where performance is everything.
I am not necessarily “bashing” on JavaScript but rather pointing out that it should not be used all the time. Especially now a days, where mobile webpages do seem to be much slower when heavy JavaScript is used. I recommend always trying to find a solution in CSS first, and then fallback on JavaScript if absolutely needed.
I think the problem is in production you’re looking for the quickest solution. It’s much easier to run to a javascript library to solve something vs figuring a CSS workaround. That’s not to say it’s right or the way it should be but at my job our priority is production.
I’m web designer in iran (lorestan state). this article is very good for me
Thank you for you
I’d call this a carousel. A slider is a control you drag within a track to set a numeric value.
Hey, thanks for the great article!
I got a simple question though: I went on caniuse.com to see the support of properties like “scroll-behavior” or “scroll-snap” and saw that it wasn’t supported on Chrome yet.
Yet I am on Chrome and can see the expected result in the Codepen you posted. Does it mean Codepen does the work itself when your browser lacks compatibility ? Thank you
Shame scroll-snap-points are deprecated (does anyone know why btw?) and don’t work in Chrome.
Excellent article. I mean, I am sure there are plaint of situations out there in which using this “limited” CSS-based slider is the key. Thank you Chris.
Hi Chris,
Only because you asked:
I believe this approach matches the construct many devs use for Tab Panels but in my opinion this is plain wrong as this navigation mechanism relates first to content, as explains in this old article on Nielsen:
In other words, jump links are meant to be used with content that requires a long scroll to discover. In my opinion, we have adopted (and defended) this construct for the only reason that it is easy to style.
In any case, “jump links” should be grouped in a list (they should be inside list items).
#petpeeve
Other than the snap points, scroll smoothing, touch support, and :target, I started making HTML and CSS only slides 7 years ago. So, aside from touch support (which wasn’t needed before touch screens were part of personal computer use) and a few nice, but not absolutely necessary for functionality, options, this method is extremely backwards compatible. Even IE6 behaves fairly well with this sort of non-JS slide carousel.
Amazing article. Thank you so much. Learned a lot from it.
I’ve built a radio/css only option, but it requires just a bit of extra markup, creating duplicate labels for the first and last slide navigation to do a full loop. It would function fine as a non-looping slider without if proper markup is vital.