Scroll Snap
Implements Tailwind’s Scroll Snap Alignment utility classes.
1
2
3
4
5
6
7
8
<div class="w-full"> <!-- Scroll Container --> <div class="snap-x scroll-px-4 snap-mandatory scroll-smooth flex gap-4 overflow-x-auto px-4 py-10"> <!-- Generate a array of 8 items; loop through each item --> { Array.from({ length: 8 }).map((_, i) => ( // Each scrollable card element <div class="snap-start shrink-0 card preset-filled py-20 w-40 md:w-80 text-center"> <span>{i + 1}</span> </div> )) } </div></div>
Carousels
Using Scroll Containers, we can create a fully functional carousel, complete with thumbnail selection.
---import { ArrowLeft, ArrowRight } from 'lucide-react';
const generatedArray = Array.from({ length: 6 });---
<div class="w-full"> <!-- Carousel --> <div class="card p-4 grid grid-cols-[auto_1fr_auto] gap-4 items-center"> <!-- Button: Left --> <button type="button" class="btn-icon preset-filled" data-carousel-left> <ArrowLeft size={16} /> </button> <!-- Full Images --> <div data-carousel class="snap-x snap-mandatory scroll-smooth flex overflow-x-auto"> <!-- Loop X many times. --> { generatedArray.map((_, i: number) => ( <img class="snap-center w-[1024px] rounded-container" src={`https://picsum.photos/seed/${i + 1}/1024/768`} alt={`full-${i}`} loading="lazy" /> )) } </div> <!-- Button: Right --> <button type="button" class="btn-icon preset-filled" data-carousel-right> <ArrowRight size={16} /> </button> </div> <!-- Thumbnails --> <div class="card p-4 grid grid-cols-6 gap-4"> <!-- Loop X many times. --> { generatedArray.map((_, i: number) => ( <button type="button" data-thumbnail> <img class="rounded-container hover:brightness-125" src={`https://picsum.photos/seed/${i + 1}/256`} alt={`thumb-${i}`} loading="lazy" /> </button> )) } </div></div>
<script> // Query Element References let elemCarousel: HTMLDivElement | null = document.querySelector('[data-carousel]'); let elemCarouselLeft: HTMLButtonElement | null = document.querySelector('[data-carousel-left]'); let elemCarouselRight: HTMLButtonElement | null = document.querySelector('[data-carousel-right]'); let elemThumbnails: NodeListOf<HTMLElement> = document.querySelectorAll('[data-thumbnail]');
// Set Left/Right arrow click handlers elemCarouselLeft?.addEventListener('click', () => carouselLeft()); elemCarouselRight?.addEventListener('click', () => carouselRight());
// Set thumbnail click handler if (elemThumbnails.length > 0) { elemThumbnails.forEach((elemButton: HTMLElement, index: number) => { elemButton?.addEventListener('click', () => carouselThumbnail(index)); }); }
/** On navigation left, scroll the container */ function carouselLeft() { if (!elemCarousel) return; const x = elemCarousel.scrollLeft === 0 ? elemCarousel.clientWidth * elemCarousel.childElementCount // loop : elemCarousel.scrollLeft - elemCarousel.clientWidth; // step left elemCarousel.scroll(x, 0); }
/** On navigation right, scroll the container */ function carouselRight() { if (!elemCarousel) return; const x = elemCarousel.scrollLeft === elemCarousel.scrollWidth - elemCarousel.clientWidth ? 0 // loop : elemCarousel.scrollLeft + elemCarousel.clientWidth; // step right elemCarousel.scroll(x, 0); }
/** On thumbnail click, scroll large image into view */ function carouselThumbnail(index: number) { if (elemCarousel) elemCarousel.scroll(elemCarousel.clientWidth * index, 0); }</script>
Multi-Column
Using Scroll Containers, we can scroll sets of items.
---import { ArrowLeft, ArrowRight } from 'lucide-react';
interface Movie { name: string; imageUrl: string; url: string;}
// Data and images via: https://www.themoviedb.org/export const movies: Movie[] = [ { name: 'The Flash', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/rktDFPbfHfUbArZ6OOOKsXcv0Bm.jpg', url: 'https://www.themoviedb.org/movie/298618-the-flash' }, { name: 'Guardians of the Galaxy Vol. 3', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/r2J02Z2OpNTctfOSN1Ydgii51I3.jpg', url: 'https://www.themoviedb.org/movie/447365-guardians-of-the-galaxy-vol-3' }, { name: 'Black Panther: Wakanda Forever', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/sv1xJUazXeYqALzczSZ3O6nkH75.jpg', url: 'https://www.themoviedb.org/movie/505642-black-panther-wakanda-forever' }, { name: 'Avengers: Infinity War', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg', url: 'https://www.themoviedb.org/movie/299536-avengers-infinity-war' }, { name: 'Spider-Man: No Way Home', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/1g0dhYtq4irTY1GPXvft6k4YLjm.jpg', url: 'https://www.themoviedb.org/movie/634649-spider-man-no-way-home' }, { name: 'The Batman', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/74xTEgt7R36Fpooo50r9T25onhq.jpg', url: 'https://www.themoviedb.org/movie/414906-the-batman' }, { name: 'Iron Man', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/78lPtwv72eTNqFW9COBYI0dWDJa.jpg', url: 'https://www.themoviedb.org/movie/1726-iron-man' }, { name: 'Venom: Let There Be Carnage', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/rjkmN1dniUHVYAtwuV3Tji7FsDO.jpg', url: 'https://www.themoviedb.org/movie/580489-venom-let-there-be-carnage' }, { name: 'Deadpool', imageUrl: 'https://www.themoviedb.org/t/p/w600_and_h900_bestv2/3E53WEZJqP6aM84D8CckXx4pIHw.jpg', url: 'https://www.themoviedb.org/movie/293660-deadpool' }];---
<div class="w-ful"> <div class="grid grid-cols-[auto_1fr_auto] gap-4 items-center"> <!-- Button: Left --> <button type="button" class="btn-icon preset-filled" data-multi-column-left> <ArrowLeft size={16} /> </button> <!-- Carousel --> <div data-multi-column class="snap-x snap-mandatory scroll-smooth flex gap-2 pb-2 overflow-x-auto"> <!-- Loop through our array of movies. --> { movies.map((movie) => ( <a href={movie.url} target="_blank" class="shrink-0 w-[28%] snap-start"> <img class="rounded-container-token hover:brightness-125" src={movie.imageUrl} alt={movie.name} title={movie.name} loading="lazy" /> </a> )) } </div> <!-- Button-Right --> <button type="button" class="btn-icon preset-filled" data-multi-column-right> <ArrowRight size={16} /> </button> </div></div>
<script> // Query Element References let elemMovies: HTMLDivElement | null = document.querySelector('[data-multi-column]')!; let elemBtnLeft: HTMLButtonElement | null = document.querySelector('[data-multi-column-left]'); let elemBtnRight: HTMLButtonElement | null = document.querySelector('[data-multi-column-right]');
// Add Button click handlers elemBtnLeft?.addEventListener('click', () => multiColumnLeft()); elemBtnRight?.addEventListener('click', () => multiColumnRight());
/** Handles the left scroll event. */ function multiColumnLeft() { if (!elemMovies) return; let x = elemMovies.scrollWidth; if (elemMovies.scrollLeft !== 0) x = elemMovies.scrollLeft - elemMovies.clientWidth; elemMovies.scroll(x, 0); }
/** Handles the right scroll event. */ function multiColumnRight() { if (!elemMovies) return; let x = 0; // -1 is used because different browsers use different methods to round scrollWidth pixels. if (elemMovies.scrollLeft < elemMovies.scrollWidth - elemMovies.clientWidth - 1) x = elemMovies.scrollLeft + elemMovies.clientWidth; elemMovies.scroll(x, 0); }</script>
Images courtesy of The Movie Database
API Reference
Learn more about Tailwind’s utility classes for scroll behavior and scroll snap.
Feature | Description |
---|---|
scroll-behavior | Controls the scroll behavior of an element. |
scroll-margin | Controls the scroll offset around items in a snap container. |
scroll-padding | Controls an element’s scroll offset within a snap container. |
scroll-snap-align | Controls the scroll snap alignment of an element. |
scroll-snap-stop | Controls whether you can skip past possible snap positions. |
scroll-snap-type | Controls how strictly snap points are enforced in a snap container. |