Web Vitals patterns

A collection of common UX patterns optimized for Core Web Vitals. This collection includes patterns that are often tricky to implement without hurting your Core Web Vitals scores. You can use the code in these examples to help ensure your projects stay on the right track.

Collection cover image

Carousels

A carousel is a UX component that displays content in a slideshow-like manner. Large, above-the-fold carousels often contain a page's Largest Contentful Paint (LCP) element, and therefore can have a significant impact on LCP. In addition, a surprising number of carousels use non-composited animations that can contribute to Cumulative Layout Shift (CLS). On pages with autoplaying carousels, this has the potential to cause infinite layout shifts.

To learn about performance and UX best practices for carousels, see Carousel Best Practices.

Autoplay carousel

<div id="carousel">
  <div id="slide-container">
    <div class="slide" data-slideIndex="0">
      <div class="slide-banner">Tour the Empire State Building! <a href="">Buy tickets now.</a></div>
      <img width="1200" height="600" src="./newyork.jpg">
    </div>
    <div class="slide" data-slideIndex="1">
      <div class="slide-banner">Ride the Shinkansen! <a href="">Buy tickets now.</a></div>
      <img width="1200" height="600" src="./tokyo.jpg">
    </div>
    <div class="slide" data-slideIndex="2">
      <div class="slide-banner">Discover relaxation! <a href="">Buy tickets now.</a></div>
      <img width="1200" height="600" src="./beach.jpg">
    </div>
    <div class="slide" data-slideIndex="3">
      <div class="slide-banner">See penguins! <a href="">Buy tickets now.</a></div>
      <img width="1200" height="600" src="./penguins.jpg">
    </div>
    <div class="slide" data-slideIndex="4">
      <div class="slide-banner">Take a ride on the wheel! <a href="">Buy tickets now.</a></div>
      <img width="1200" height="600" src="./wheel.jpg">
    </div>
  </div>
  <div id="back-button" class="arrow back"></div>
  <div id="forward-button" class="arrow forward"></div>
  <div class="slide-indicators">
    <div class="slide-indicator active"></div>
    <div class="slide-indicator"></div>
    <div class="slide-indicator"></div>
    <div class="slide-indicator"></div>
    <div class="slide-indicator"></div>
  </div>
</div>
function autoplayCarousel() {
    const carouselEl = document.getElementById("carousel");
    const slideContainerEl = carouselEl.querySelector("#slide-container");
    const slideEl = carouselEl.querySelector(".slide");
    let slideWidth = slideEl.offsetWidth;
    // Add click handlers
    document.querySelector("#back-button")
        .addEventListener("click", () => navigate("backward"));
    document.querySelector("#forward-button")
        .addEventListener("click", () => navigate("forward"));
    document.querySelectorAll(".slide-indicator")
        .forEach((dot, index) => {
            dot.addEventListener("click", () => navigate(index));
            dot.addEventListener("mouseenter", () => clearInterval(autoplay));
        });
    // Add keyboard handlers
    document.addEventListener('keydown', (e) => {
        if (e.code === 'ArrowLeft') {
            clearInterval(autoplay);
            navigate("backward");
        } else if (e.code === 'ArrowRight') {
            clearInterval(autoplay);
            navigate("forward");
        }
    });
    // Add resize handler
    window.addEventListener('resize', () => {
        slideWidth = slideEl.offsetWidth;
    });
    // Autoplay
    const autoplay = setInterval(() => navigate("forward"), 3000);
    slideContainerEl.addEventListener("mouseenter", () => clearInterval(autoplay));
    // Slide transition
    const getNewScrollPosition = (arg) => {
        const gap = 10;
        const maxScrollLeft = slideContainerEl.scrollWidth - slideWidth;
        if (arg === "forward") {
            const x = slideContainerEl.scrollLeft + slideWidth + gap;
            return x <= maxScrollLeft ? x : 0;
        } else if (arg === "backward") {
            const x = slideContainerEl.scrollLeft - slideWidth - gap;
            return x >= 0 ? x : maxScrollLeft;
        } else if (typeof arg === "number") {
            const x = arg * (slideWidth + gap);
            return x;
        }
    }
    const navigate = (arg) => {
        slideContainerEl.scrollLeft = getNewScrollPosition(arg);
    }
    // Slide indicators
    const slideObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const slideIndex = entry.target.dataset.slideindex;
                carouselEl.querySelector('.slide-indicator.active').classList.remove('active');
                carouselEl.querySelectorAll('.slide-indicator')[slideIndex].classList.add('active');
            }
        });
    }, { root: slideContainerEl, threshold: .1 });
    document.querySelectorAll('.slide').forEach((slide) => {
        slideObserver.observe(slide);
    });
}
autoplayCarousel();
#carousel {
    max-width: 1200px;
    display: flex;
    flex-direction: column;
    margin: 0 auto;
    position: relative;
}  
.slide-indicators {
    display: flex;
    justify-content: center;
}
.slide-indicator {
    height: 44px;
    width: 50px;
    display: flex;
    justify-items: center;
    cursor: pointer;
}
.slide-indicator:after {
    content: "";
    background-color: #878787;
    height: 10px;
    margin-top: 10px;
    width: 40px;
}
.slide-indicator.active:after,
.slide-indicator:hover:after {
    background-color: #000000;
}
.slide-banner {
    background-color: #000000;
    color: #ffffff;
    position: absolute;
    left: 0;
    bottom: 20px;
    padding: 15px;
    font-size: 2.5vw;
}
.slide-banner a {
    color: #ffffff;
}
#slide-container {
    scroll-snap-type: x mandatory;
    overflow-x: scroll;
    overflow-y: hidden;
    display: flex;
    align-items: center;
    height: 100%;
    gap: 10px;
    -webkit-overflow-scrolling: touch;
    scroll-behavior: smooth;
}
.slide {
    scroll-snap-align: center;
    position: relative;
    min-width: 100%;
    padding-top: 50%;
}
.slide img {
    height: 100%;
    width: auto;
    position: absolute;
    top: 0;
    left: 0;
}
.arrow {
    color: #ffffff;
    height: 20px;
    width: 20px;
    background-color: #000000;
    position: absolute;
    padding: 10px;
    opacity: .3;
    cursor: pointer;
}
.arrow.back {
    left: 10px;
    top: 10px;
}
.arrow.forward {
    right: 10px;
    top: 10px;
}
.arrow:hover {
    opacity: 1;
}

This carousel uses CSS scroll snap to create smooth, performant slide transitions that do not cause layout shifts.

Learn more