Simple CSS slider with scroll-snapping

– Published 24th Jan, 2022

Let's whip up a quick carousel using modern CSS!

TLDR: Here's a Codepen.

I've started a new project and just have a plain index.html file.

/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS Slider</title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>

</body>
</html>

I've also created a file called style.css, which you can see is linked above. Recently with framework-less projects, I've started using "The New CSS Reset" by Elad Schecter. I like how minimal it is, and how it removes margins by default. Normalize will work fine also for this use case if you prefer that.

Add the reset to your html with the following:

/index.html

<link rel="stylesheet" href="/reset.css">

/reset.css

/*** The new CSS Reset - version 1.4.5 (last updated 13.1.2022) ***/

/*
    Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
    - The "symbol *" part is to solve Firefox SVG sprite bug
 */
 *:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) {
    all: unset;
    display: revert;
}

/* Preferred box-sizing value */
*,
*::before,
*::after {
    box-sizing: border-box;
}

/* Reapply the pointer cursor for anchor tags */
a {
    cursor: revert;
}

/* Remove list styles (bullets/numbers) */
ol, ul, menu {
    list-style: none;
}

/* For images to not be able to exceed their container */
img {
    max-width: 100%;
}

/* removes spacing between cells in tables */
table {
    border-collapse: collapse;
}

/* revert the 'white-space' property for textarea elements on Safari */
textarea {
    white-space: revert;
}

/* fix the feature of 'hidden' attribute.
   display:revert; revert to element instead of attribute */
:where([hidden]){
    display:none;
}

/* revert for bug in Chromium browsers
   - fix for the content editable attribute will work properly. */
:where([contenteditable]){
    -moz-user-modify: read-write;
    -webkit-user-modify: read-write;
    overflow-wrap: break-word;
    -webkit-line-break: after-white-space;
}

/* apply back the draggable feature - exist only in Chromium and Safari */
:where([draggable="true"]) {
    -webkit-user-drag: element;
}

Now we'll add some markup for our slider..

/index.html

<div class="slider">
        <div class="slider__slides">
            <figure class="slider__slide">
                <img src="https://picsum.photos/1200/800" alt="">
            </figure>
            <figure class="slider__slide">
                <img src="https://picsum.photos/1000/900" alt="">
            </figure>
            <figure class="slider__slide">
                <img src="https://picsum.photos/1400/1400" alt="">
            </figure>
        </div>
    </div>

I'm using Picsum here for images, which is great for placeholders.

Now let's style this thing.

Here's the base CSS, which I'll run through next.

/style.css

.slider {
    display: flex;
    align-items: center;
    position: relative;
}

.slider__slides {
    max-height: 80vh;
    position: relative;
    display: flex;
    align-items: center;
    gap: 3rem;
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
}

.slider__slide {
    scroll-snap-align: start;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100vw;
}

.slider__slide > img {
    max-width: 100%;
    height: 100%;
    object-fit: contain;
}

We've essentially got a wrapper div (.slider), and then a separater div for our slides within it. I've created the wrapper as I'll be adding some nav with JS later on.

The key to this file are the following lines..

.slider__slides {
    /* ... */
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
}

.slider__slide {
    scroll-snap-align: start;
    /* ... */
}

With these 4 lines together, we're creating a "scroll snapping" effect, that leverages the browser's native scroll functionality – with no Javascript!

The rest of the code is just defaults for the slider. I've set a "max-height" of "80vh", but this can be whatever you like.

Let's add a full-screen modifier

Would be nice to use this same code, but have the slider full-screen too. Nice and easy, we'll keep the BEM style here and add the following to our CSS:

/style.css

.slider--fullscreen {
    height: 100vh;
}

.slider--fullscreen .slider__slides {
    max-height: 100vh;
    gap: 0;
}

.slider--fullscreen .slider__slide {
    height: 100vh;
}

.slider--fullscreen .slider__slide img {
    object-fit: cover;
    width: 100%;
    height: 100%;
}

Now all we have to do in our markup is add the additional "slider--fullscreen" to our top level wrapper.

./index.html

<body>
    <div class="slider slider--fullscreen">
    <!-- ... -->

Creating a nicer user experience with Javascript

JS obviously isn't required up until this point, but it'd be nice to have some navigation buttons for the slider. We'll add a nav block, and include a JS file before the closing body tag.

/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Scroll Snap</title>
    <link rel="stylesheet" href="/reset.css">
    <link rel="stylesheet" href="/style.css">
</head>
<body>
    <div class="slider slider--fullscreen">
        <nav class="slider__nav">
            <button title="Go to the previous slide" data-prev>
                &larr;
            </button>
            <button title="Go to the next slide" data-next>
                &rarr;
            </button>
        </nav>

        <div class="slider__slides" data-slider>
            <figure class="slider__slide">
                <img src="https://picsum.photos/1200/800" alt="">
            </figure>
            <figure class="slider__slide">
                <img src="https://picsum.photos/1000/900" alt="">
            </figure>
            <figure class="slider__slide">
                <img src="https://picsum.photos/1400/1400" alt="">
            </figure>
        </div>
    </div>

    <script src="/app.js"></script>
</body>
</html>

I've kept this very minimal for the sake of markup that's easy to read, but the key is just that you have some buttons with "data-prev" and "data-next" attributes respectively.

The Javascript is very simple too, no need for any frameworks or complicated state-tracking here!

const slider = document.querySelector('[data-slider]');
const prevButton = document.querySelector('[data-prev]');
const nextButton = document.querySelector('[data-next]');

function slide(direction) {
    let left;
    const { scrollLeft, clientWidth } = slider;

    switch (direction) {
        case 'prev':
            left = scrollLeft - clientWidth;
            break;
        case 'next':
        default:
            left = scrollLeft + clientWidth;
            break;
    }

    slider.scroll({
        left,
        behavior: 'smooth',
    });
}

if (slider && prevButton && nextButton) {
    prevButton.addEventListener('click', () => slide('prev'));
    nextButton.addEventListener('click', () => slide('next'));
}

I've used a switch statement here to handle sliding either direction in a single function, but you could just as easily add a separate "prevSlide" and "nextSlide" function if that reads better to you.

What we're doing is just getting the current scroll position within the slider, and then adding the slider's width to it (i.e. one full screen width). By using the "behaviour: 'smooth'" param, we're leveraging the browser's defaults and don't need to add any custom animations.

And there we have it!

A fairly nice looking slider with minimal code that leverages the platform. You can extend this and customise it however you like. I'd add gestures next, so that you can drag with your mouse. This already works on mobile as you're simply scrolling.

Then perhaps adding some more Javascript to "scroll to start" when the user clicks the "next" button from the last slide.