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 –

So, here’s how I replicated it for my homepage.
Carousel Demo
JavaScript Game: Obstakl
Obstakl is a game where the player needs to avoid moving obstacles, by controlling the main character using the mouse. Play full-screen Obstakl is not a game, but a showcase of what…
JavaScript Marquee: A collection of scrolling text snippets
I have started to consider adding scrolling text into my design process. I started by using a CSS-only approach. The principle for the CSS infinite scrolling process is to have a specific…
JavaScript Numbers: A Modern Reference
JavaScript is a dynamically typed language, and it doesn’t have specific integer or floating-point types, unlike some other languages. In JavaScript, all numbers are represented as 64-bit (8 bytes) floating-point numbers, which…
How to Enable JavaScript
How to enable JavaScript in your browser and why Nowadays, all web pages contain JavaScript, a scripting programming language that runs on the visitor’s web browser (client-side). It makes web pages functional…
Fast and accessible JavaScript client logo carousel
A while ago, I had to create a client logo carousel using CSS only. I did, and it worked perfectly. I tried to use it on several other projects after that, and…
Segmented Horizontal Bar Chart (Graph) Using Vanilla JavaScript
If you're looking to create a segmented (stepped) horizontal bar chart using nothing but vanilla JavaScript, you're in luck!
A simple JavaScript suggestion autocomplete app
These days, I’ve been working on building a prediction search engine for real estate websites. I started with something small, and I am happy with what I got so far. I wouldn’t…
A collection of modern native JavaScript object and array utilities
Arrays Chunk Creates an array of elements split into groups the length of size. Compact Creates an array with all falsy values removed. Concatenate Creates a new array concatenating additional arrays and/or…
How to set up your own Google CrUX report
As part of decommissioning the Core Web Vitals report from Lighthouse, here’s how the CrUX report has been built. If you are wondering why am I removing these features, here’s why –…
JavaScript Canvas Sine Wave
The code below generates a sine wave with a specified amplitude, frequency, and phase. The wave is drawn on the Canvas using the lineTo method, which creates a line from the current…
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.

Technical SEO specialist, JavaScript developer and senior full-stack developer. Owner of getButterfly.com.
If you like this article, go ahead and follow me on X or buy me a coffee to support my work!
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.
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 :)
Hi Ciprian,
How would you go about implementing the same carousel with “linear interpolation” instead for the easing?
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.