Scroll Snap
Implements Tailwind’s Scroll Snap Alignment utility classes.
1
2
3
4
5
6
7
8
export default function Default() {
return (
<div className="w-full">
{/* Scroll Container */}
<div className="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 key={i} className="snap-start shrink-0 card preset-filled py-20 w-40 md:w-80 text-center">
<span>{i + 1}</span>
</div>
))}
</div>
</div>
);
}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 -->
{#each Array.from({ length: 8 }) as _, 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>
{/each}
</div>
</div>Carousels
Using Scroll Containers, we can create a fully functional carousel, complete with thumbnail selection.
import { useRef } from 'react';
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
export default function Carousel() {
const generatedArray = Array.from({ length: 6 });
const elemCarouselRef = useRef<HTMLDivElement>(null);
function carouselLeft() {
if (!elemCarouselRef.current) return;
const elemCarousel = elemCarouselRef.current;
const x =
elemCarousel.scrollLeft === 0
? elemCarousel.clientWidth * elemCarousel.childElementCount // loop
: elemCarousel.scrollLeft - elemCarousel.clientWidth; // step left
elemCarousel.scroll(x, 0);
}
function carouselRight() {
if (!elemCarouselRef.current) return;
const elemCarousel = elemCarouselRef.current;
const x =
elemCarousel.scrollLeft === elemCarousel.scrollWidth - elemCarousel.clientWidth
? 0 // loop
: elemCarousel.scrollLeft + elemCarousel.clientWidth; // step right
elemCarousel.scroll(x, 0);
}
function carouselThumbnail(index: number) {
if (elemCarouselRef.current) {
elemCarouselRef.current.scroll(elemCarouselRef.current.clientWidth * index, 0);
}
}
return (
<div className="w-full">
{/* Carousel */}
<div className="card p-4 grid grid-cols-[auto_1fr_auto] gap-4 items-center">
{/* Button: Left */}
<button type="button" className="btn-icon preset-filled" onClick={carouselLeft} title="Previous slide" aria-label="Previous slide">
<ArrowLeftIcon size={16} />
</button>
{/* Full Images */}
<div ref={elemCarouselRef} className="snap-x snap-mandatory scroll-smooth flex overflow-x-auto">
{/* Loop X many times. */}
{generatedArray.map((_, i) => (
<img
key={i}
className="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" className="btn-icon preset-filled" onClick={carouselRight} title="Next slide" aria-label="Next slide">
<ArrowRightIcon size={16} />
</button>
</div>
{/* Thumbnails */}
<div className="card p-4 grid grid-cols-6 gap-4">
{/* Loop X many times. */}
{generatedArray.map((_, i) => (
<button key={i} type="button" onClick={() => carouselThumbnail(i)}>
<img
className="rounded-container hover:brightness-125"
src={`https://picsum.photos/seed/${i + 1}/256`}
alt={`thumb-${i}`}
loading="lazy"
/>
</button>
))}
</div>
</div>
);
}<script lang="ts">
import { ArrowLeftIcon, ArrowRightIcon } from '@lucide/svelte';
const generatedArray = Array.from({ length: 6 });
let elemCarousel: HTMLDivElement;
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);
}
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);
}
function carouselThumbnail(index: number) {
if (elemCarousel) {
elemCarousel.scroll(elemCarousel.clientWidth * index, 0);
}
}
</script>
<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" onclick={carouselLeft} title="Previous slide" aria-label="Previous slide">
<ArrowLeftIcon size={16} />
</button>
<!-- Full Images -->
<div bind:this={elemCarousel} class="snap-x snap-mandatory scroll-smooth flex overflow-x-auto">
<!-- Loop X many times. -->
{#each generatedArray as _, i}
<img
class="snap-center w-[1024px] rounded-container"
src={`https://picsum.photos/seed/${i + 1}/1024/768`}
alt={`full-${i}`}
loading="lazy"
/>
{/each}
</div>
<!-- Button: Right -->
<button type="button" class="btn-icon preset-filled" onclick={carouselRight} title="Next slide" aria-label="Next slide">
<ArrowRightIcon size={16} />
</button>
</div>
<!-- Thumbnails -->
<div class="card p-4 grid grid-cols-6 gap-4">
<!-- Loop X many times. -->
{#each generatedArray as _, i}
<button type="button" onclick={() => carouselThumbnail(i)}>
<img
class="rounded-container hover:brightness-125"
src={`https://picsum.photos/seed/${i + 1}/256`}
alt={`thumb-${i}`}
loading="lazy"
/>
</button>
{/each}
</div>
</div>Multi-Column
Using Scroll Containers, we can scroll sets of items.
import { useRef } from 'react';
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
interface Movie {
name: string;
imageUrl: string;
url: string;
}
// Data and images via: https://www.themoviedb.org/
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',
},
];
export default function MultiColumn() {
const elemMoviesRef = useRef<HTMLDivElement>(null);
function multiColumnLeft() {
if (!elemMoviesRef.current) return;
const elemMovies = elemMoviesRef.current;
let x = elemMovies.scrollWidth;
if (elemMovies.scrollLeft !== 0) {
x = elemMovies.scrollLeft - elemMovies.clientWidth;
}
elemMovies.scroll(x, 0);
}
function multiColumnRight() {
if (!elemMoviesRef.current) return;
const elemMovies = elemMoviesRef.current;
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);
}
return (
<div className="w-full">
<div className="grid grid-cols-[auto_1fr_auto] gap-4 items-center">
{/* Button: Left */}
<button type="button" className="btn-icon preset-filled" onClick={multiColumnLeft} title="Scroll left" aria-label="Scroll left">
<ArrowLeftIcon size={16} />
</button>
{/* Carousel */}
<div ref={elemMoviesRef} className="snap-x snap-mandatory scroll-smooth flex gap-2 pb-2 overflow-x-auto">
{/* Loop through our array of movies. */}
{movies.map((movie) => (
<a key={movie.name} href={movie.url} target="_blank" className="shrink-0 w-[28%] snap-start">
<img
className="rounded-container-token hover:brightness-125"
src={movie.imageUrl}
alt={movie.name}
title={movie.name}
loading="lazy"
/>
</a>
))}
</div>
{/* Button-Right */}
<button type="button" className="btn-icon preset-filled" onClick={multiColumnRight} title="Scroll right" aria-label="Scroll right">
<ArrowRightIcon size={16} />
</button>
</div>
</div>
);
}<script lang="ts">
import { ArrowLeftIcon, ArrowRightIcon } from '@lucide/svelte';
interface Movie {
name: string;
imageUrl: string;
url: string;
}
// Data and images via: https://www.themoviedb.org/
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',
},
];
let elemMovies: HTMLDivElement;
function multiColumnLeft() {
if (!elemMovies) return;
let x = elemMovies.scrollWidth;
if (elemMovies.scrollLeft !== 0) {
x = elemMovies.scrollLeft - elemMovies.clientWidth;
}
elemMovies.scroll(x, 0);
}
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>
<div class="w-full">
<div class="grid grid-cols-[auto_1fr_auto] gap-4 items-center">
<!-- Button: Left -->
<button type="button" class="btn-icon preset-filled" onclick={multiColumnLeft} title="Scroll left" aria-label="Scroll left">
<ArrowLeftIcon size={16} />
</button>
<!-- Carousel -->
<div bind:this={elemMovies} class="snap-x snap-mandatory scroll-smooth flex gap-2 pb-2 overflow-x-auto">
<!-- Loop through our array of movies. -->
{#each movies as 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>
{/each}
</div>
<!-- Button-Right -->
<button type="button" class="btn-icon preset-filled" onclick={multiColumnRight} title="Scroll right" aria-label="Scroll right">
<ArrowRightIcon size={16} />
</button>
</div>
</div>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. |








