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.
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.
The Future
I intend to play with the carousel width, as the fullwidth works unless I have a really wide monitor. Then it looks strange. I like Stripe’s design, as the carousel starts on the left of the content, and is shows as many articles are available (even 1 or 2 would look good). In my case, I know I have more than 10 articles at any time, so this is not an issue. But I’m measuring user engagement, and I’ll probably make some changes in the next 30 days.
Related posts
If you like this article, go ahead and follow me on Twitter or buy me a coffee to support my work!