You can get pretty far in making a slider with just HTML and CSS

Avatar of Chris Coyier
Chris Coyier on (Updated on )

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>

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>

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 the animation-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.