How to Create a Draggable Carousel using Vanilla JavaScript

Ciprian on Thursday, October 21, 2021 in JavaScript Carousels

NEW! Learn JavaScript by example. Code snippets, how-to's and tutorials. Try now!

A few months ago I added a new, fancy JavaScript carousel to my homepage. It looked and behaved great, but it caused a massive CLS (Cumulative Layout Shift) score and Google was not happy.

đź‘‹ I wrote about JavaScript sliders and carousels before, and I like getting them as small as possible, codewise, and using as much native behaviour as I can.

This time, I replaced it with a draggable carousel I saw and liked on the Stripe blog

JavaScript Draggable Carousel
JavaScript Draggable Carousel

So, here’s how I replicated it for my homepage.

SEO Crawlability Issue: Resources are formatted as page link

While working on a Semrush audit, I found a new type of crawlability notice: 47 resources are formatted as page link According to their documentation, “we detected that some links to resources are…

A Repository of Fine JavaScript Libraries and Components

Today, I launched my personal repository of JavaScript libraries and components. All scripts and libraries in this repository have been created in-house, either as client work or simply as a demo. The…

StackGrid – A Light Vanilla JavaScript Masonry Grid

Introducing the lightest and most efficient Masonry grid available – StackGrid – powered by pure vanilla JavaScript, no dependencies required. This grid is perfect for those who want a lightweight and easy-to-implement…

How to Open Only One Details/Summary Element at a Time

Let’s suppose you want to build a simple accordion or a help section, and you want to use a <details> element and only have one open at a time. Here is how…

Canvas – A Simple Drawing Application

This is a simple drawing application that allows the user to select a colour and thickness for their brush and draw on a canvas element. The user can also save and clear…

Canvas BĂ©zier Curves

This is a demo for the Canvas: An Awesome Introduction article. This demonstration shows how BĂ©zier curves can be drawn on a canvas element. Drag the line ends, or the control points…

Code Golfing Tips & Tricks: How to Minify your JavaScript Code

JavaScript golfing is the process of writing the smallest amount of JavaScript code to do something awesome.

The Complete JavaScript Strings Reference

JavaScript strings are deceptively complex constructs. There are actually two different types of strings – string Literals and string Objects – and they both behave somewhat differently, even while trying to masquerade…

How to code your own JavaScript de-duplicator

I have a new tool on getButterfly: a data de-duplicator for emails, URLs, IDs, names and more. It's written in JavaScript, with no external (or server-side) dependencies.

JavaScript Form Validation

This JavaScript code snippet adds inline validation to any form field (input, select and textarea). Feel free to customise it as you see fit.

The WordPress Loop

I am getting the last 10 posts in the JavaScript category, I am setting up a counter and changing background colours based on this counter. Hacky, I know, I could have done this in JavaScript, or in CSS, using nth-child selectors.

<?php
function whiskey_carousel() {
    $args = [
        'post_status' => 'publish',
        'post_type' => 'post',
        'posts_per_page' => 10,
        'category_name' => 'javascript'
    ];

    $featuredQuery = new WP_Query($args);

    $colours = [
        '#02bcf5', '#0073e6', '#003ab9', '#635bff', '#002c59', '#09cbcb',
        '#02bcf5', '#0073e6', '#003ab9', '#635bff', '#002c59', '#09cbcb',
    ];
    $i = 0;

    $data = '<div class="whiskey-cards alignfull">';

        if ($featuredQuery->have_posts()) {
            while ($featuredQuery->have_posts()) {
                $featuredQuery->the_post();

                $postID = get_the_ID();
                $excerpt = html_entity_decode(wp_trim_words(get_the_excerpt(), 32));

                $data .= '<div class="whiskey-card" style="background-color: ' . $colours[$i] . ';">
                    <h3><a href="' . get_permalink($postID) . '">' . get_the_title($postID) . '</a></h3>
                    <p class="whiskey-card--content">' . $excerpt . '</p>
                    <p class="whiskey-card--link"><a href="' . get_permalink($postID) . '">Learn more <svg class="HoverArrow" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><g fill-rule="evenodd"><path class="HoverArrow__linePath" d="M0 5h7"></path><path class="HoverArrow__tipPath" d="M1 1l4 4-4 4"></path></g></svg></a></p>
                </div>';

                $i++;
            }
        }

    $data .= '</div>';

    return $data;
}

add_shortcode('whiskey-carousel', 'whiskey_carousel');

The JavaScript Code

The JavaScript features momentum scrolling (mouse wheel) and (almost) native HTML dragging behaviour. The dragging is also available to mobile devices, as the content is overflowing horizontally.

document.addEventListener('DOMContentLoaded', () => {
    if (document.querySelector('.whiskey-cards')) {
        // Slider dragging
        const slider = document.querySelector('.whiskey-cards');
        let isDown = false;
        let startX;
        let scrollLeft;

        slider.addEventListener('mousedown', (e) => {
            isDown = true;
            slider.classList.add('active');
            startX = e.pageX - slider.offsetLeft;
            scrollLeft = slider.scrollLeft;
            cancelMomentumTracking();
        });

        slider.addEventListener('mouseleave', () => {
            isDown = false;
            slider.classList.remove('active');
        });

        slider.addEventListener('mouseup', () => {
            isDown = false;
            slider.classList.remove('active');
            beginMomentumTracking();
        });

        slider.addEventListener('mousemove', (e) => {
            if (!isDown) return;
            e.preventDefault();
            const x = e.pageX - slider.offsetLeft;
            const walk = (x - startX); //scroll-fast
            var prevScrollLeft = slider.scrollLeft;
            slider.scrollLeft = scrollLeft - walk;
            velX = slider.scrollLeft - prevScrollLeft;
        });

        // Momentum 
        var velX = 0;
        var momentumID;

        slider.addEventListener('wheel', (e) => {
            cancelMomentumTracking();
        });

        function beginMomentumTracking() {
            cancelMomentumTracking();
            momentumID = requestAnimationFrame(momentumLoop);
        }

        function cancelMomentumTracking() {
            cancelAnimationFrame(momentumID);
        }

        function momentumLoop() {
            slider.scrollLeft += velX * 2;
            velX *= 0.95;
            if (Math.abs(velX) > 0.5) {
                momentumID = requestAnimationFrame(momentumLoop);
            }
        }

        // Scroll
        const scrollContainer = document.querySelector(".whiskey-cards");

        scrollContainer.addEventListener("wheel", (evt) => {
            evt.preventDefault();

            window.requestAnimationFrame(() => {
                scrollContainer.scrollTo({ top: 0, left: scrollContainer.scrollLeft + (evt.deltaY * 2), behavior: "smooth" });
            });
        });
    }
});

The CSS Style

There are lots of opinionated styles below, so make sure you get what you need.

.whiskey-cards {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: scroll;
    -webkit-overflow-scrolling: touch;
    -ms-overflow-style: none;
    scrollbar-width: none;

    padding: 48px 48px 0 48px;

}
.whiskey-cards::-webkit-scrollbar {
    -webkit-appearance: none;
    width: 5px;
    height: 5px;
}
.whiskey-cards::-webkit-scrollbar-thumb {
    border-radius: 0;
    background-color: rgba(0, 0, 0, .5);
    background: linear-gradient(90deg, #02bcf5, #0073e6, #003ab9, #635bff);
    box-shadow: 0 0 1px rgba(255, 255, 255, .5);
    border-radius: 16px;
    opacity: .5;
}
.whiskey-cards:hover::-webkit-scrollbar-thumb {
    opacity: 1;
}

.whiskey-card {
    display: flex;
    flex-direction: column;
    min-width: 244px;
    flex-basis: 244px;
    border-radius: 16px;
    margin: 8px;
    padding: 16px;
    box-shadow: 0 -16px 24px rgb(0 0 0 / 5%);
    color: #ffffff;

    transition: all 150ms cubic-bezier(0.215,0.61,0.355,1);
}
.whiskey-card:hover {
    background-color: #0a2540 !important;
    transform: scale(1.04) translateY(-16px);
    box-shadow: 0 -16px 24px rgb(0 0 0 / 10%);
}
.whiskey-card h3 {
    padding-top: 0;
    line-height: 1.35;
}
.whiskey-card .whiskey-card--content {
    line-height: 1.5;
    font-size: 15px;
    font-weight: 300;
}
.whiskey-card .whiskey-card--link {
    line-height: 1.5;
    font-size: 15px;
    font-weight: 700;
    opacity: .7;
    margin: auto 0 0 0;
}
.whiskey-card h3 a,
.whiskey-card .whiskey-card--link a {
    color: #ffffff;
}
.whiskey-card .whiskey-card--link a svg {
    --arrowSpacing: 5px;
    --arrowHoverTransition: 150ms cubic-bezier(0.215,0.61,0.355,1);
    --arrowHoverOffset: translateX(3px);
    --arrowTipTransform: none;
    --arrowLineOpacity: 0;
    position: relative;
    top: 1px;
    margin-left: var(--arrowSpacing);
    stroke-width: 2px;
    fill: none;
    stroke: currentColor;
}
.HoverArrow__linePath {
    opacity: var(--arrowLineOpacity);
    transition: opacity var(--hoverTransition,var(--arrowHoverTransition));
}
.HoverArrow__tipPath {
    transform: var(--arrowTipTransform);
    transition: transform var(--hoverTransition,var(--arrowHoverTransition));
}
.whiskey-card:hover .HoverArrow__linePath {
    --arrowLineOpacity: 1;
}
.whiskey-card:hover .HoverArrow__tipPath {
    --arrowTipTransform: var(--arrowHoverOffset);
}

I have tried using CSS scroll snapping, but this wouldn’t work with the mouse wheel, as the element.scrollTo() function expected a certain, fixed scroll value.

Also, as my carousel is not 100% smooth, I had to change the amount of mouse wheel scrolling, as seen in this line:

scrollContainer.scrollTo({ top: 0, left: scrollContainer.scrollLeft + (evt.deltaY * 2), behavior: "smooth" });

evt.deltaY was not enough, so I doubled it: evt.deltaY * 2. I’m happy with it so far, but I know there’s room for improvement, as the actual scrolling amount can be further tweaked.

Related posts

4 comments on “How to Create a Draggable Carousel using Vanilla JavaScript

  1. Hi,

    I really like your carousel, but there’s only one feature I feel your carousel is missing and that’s navigation, especially on larger displays. Could you please add some navigation arrows to make it easier to control the carousel and hide the corresponding arrow when it reaches the start/end of the container?

    To be more specific, I’m looking for a carousel that is shown in google search results (g-scrolling-carousel). Here’re some examples:
    https://trendyol.github.io/react-carousel/docs/scrolling-carousel/
    https://www.jqueryscript.net/demo/google-scrolling-carousel/
    https://jimmythompson.me/project/d-carousel/

    Any chance of some help with this? Thanks.

    1. With my current carousel, I’m trying to keep JavaScript to a minimum. To be honest, in 2022, there’s no need for arrows, as users can see the carousel and swipe it, or drag to scroll. It’s natural behaviour.

      That being said, the carousel was only for my homepage, so I don’t have any plans of working on it, unless it’s to remove more code :)

  2. Hi Ciprian,

    How would you go about implementing the same carousel with “linear interpolation” instead for the easing?

    1. I would probably play with the code below:

      slider.scrollLeft += velX * 2;
      velX *= 0.95;
      if (Math.abs(velX) > 0.5) {
      momentumID = requestAnimationFrame(momentumLoop);
      }

      and adjust the velocity.

Leave a Reply

Your email address will not be published. Required fields are marked *