Back to Blog
CSSScroll SnapUXCarousel

CSS Scroll Snap Guide

Create smooth scrolling experiences with CSS Scroll Snap. From carousels to page sections to galleries.

B
Bootspring Team
Engineering
April 18, 2021
5 min read

Scroll snap creates controlled scrolling experiences without JavaScript. Here's how to implement common patterns.

Basic Scroll Snap#

1/* Container */ 2.scroll-container { 3 scroll-snap-type: x mandatory; 4 overflow-x: auto; 5 display: flex; 6} 7 8/* Items */ 9.scroll-item { 10 scroll-snap-align: start; 11 flex-shrink: 0; 12 width: 100%; 13} 14 15/* Snap types */ 16.mandatory { 17 /* Always snaps to nearest snap point */ 18 scroll-snap-type: x mandatory; 19} 20 21.proximity { 22 /* Only snaps when close to snap point */ 23 scroll-snap-type: x proximity; 24} 25 26/* Axes */ 27.horizontal { 28 scroll-snap-type: x mandatory; 29} 30 31.vertical { 32 scroll-snap-type: y mandatory; 33} 34 35.both { 36 scroll-snap-type: both mandatory; 37}
1.carousel { 2 display: flex; 3 overflow-x: auto; 4 scroll-snap-type: x mandatory; 5 scroll-behavior: smooth; 6 gap: 1rem; 7 8 /* Hide scrollbar */ 9 scrollbar-width: none; 10 -ms-overflow-style: none; 11} 12 13.carousel::-webkit-scrollbar { 14 display: none; 15} 16 17.carousel-item { 18 flex: 0 0 300px; 19 scroll-snap-align: start; 20} 21 22/* Center alignment */ 23.carousel-centered { 24 scroll-snap-type: x mandatory; 25 scroll-padding-inline: calc(50% - 150px); 26} 27 28.carousel-centered .item { 29 scroll-snap-align: center; 30} 31 32/* Peek next item */ 33.carousel-peek { 34 padding-inline: 1rem; 35} 36 37.carousel-peek .item { 38 flex: 0 0 calc(100% - 4rem); 39 scroll-snap-align: center; 40}

Full-Page Sections#

1.fullpage { 2 height: 100vh; 3 overflow-y: auto; 4 scroll-snap-type: y mandatory; 5} 6 7.section { 8 height: 100vh; 9 scroll-snap-align: start; 10} 11 12/* Stop at specific point */ 13.section-with-header { 14 scroll-snap-align: start; 15 scroll-margin-top: 60px; /* Account for fixed header */ 16} 17 18/* Alternative: scroll-padding on container */ 19.fullpage-with-header { 20 scroll-padding-top: 60px; 21 scroll-snap-type: y mandatory; 22}
1.gallery { 2 display: grid; 3 grid-auto-flow: column; 4 grid-auto-columns: 100%; 5 overflow-x: auto; 6 scroll-snap-type: x mandatory; 7 scroll-behavior: smooth; 8} 9 10.gallery-image { 11 scroll-snap-align: center; 12 width: 100%; 13 height: 100%; 14 object-fit: cover; 15} 16 17/* With thumbnails */ 18.gallery-main { 19 scroll-snap-type: x mandatory; 20 overflow-x: auto; 21} 22 23.gallery-thumb { 24 cursor: pointer; 25 opacity: 0.6; 26 transition: opacity 0.2s; 27} 28 29.gallery-thumb:hover, 30.gallery-thumb.active { 31 opacity: 1; 32}

Card Slider#

1.card-slider { 2 display: flex; 3 overflow-x: auto; 4 scroll-snap-type: x mandatory; 5 gap: 1rem; 6 padding: 1rem; 7} 8 9/* Cards that show partial next card */ 10.card-slider .card { 11 flex: 0 0 calc(100% - 3rem); 12 scroll-snap-align: start; 13} 14 15@media (min-width: 768px) { 16 .card-slider .card { 17 flex: 0 0 calc(50% - 1rem); 18 } 19} 20 21@media (min-width: 1024px) { 22 .card-slider .card { 23 flex: 0 0 calc(33.333% - 1rem); 24 } 25}

Snap Stop#

1/* Prevent skipping items on fast scroll */ 2.item { 3 scroll-snap-align: start; 4 scroll-snap-stop: always; /* Stop at every item */ 5} 6 7/* Default behavior (can skip items) */ 8.item-skip { 9 scroll-snap-align: start; 10 scroll-snap-stop: normal; 11}

Scroll Padding and Margin#

1/* Container padding */ 2.container { 3 scroll-snap-type: x mandatory; 4 scroll-padding-inline: 2rem; 5} 6 7/* Item margin */ 8.item { 9 scroll-snap-align: start; 10 scroll-margin-inline-start: 1rem; 11} 12 13/* Use case: Fixed header */ 14.page-sections { 15 scroll-snap-type: y mandatory; 16 scroll-padding-top: 80px; /* Header height */ 17} 18 19.section { 20 scroll-snap-align: start; 21}

Horizontal Timeline#

1.timeline { 2 display: flex; 3 overflow-x: auto; 4 scroll-snap-type: x mandatory; 5 padding: 2rem; 6} 7 8.timeline-item { 9 flex: 0 0 300px; 10 scroll-snap-align: center; 11 position: relative; 12 padding: 2rem; 13} 14 15.timeline-item::before { 16 content: ''; 17 position: absolute; 18 top: 50%; 19 left: 0; 20 right: 0; 21 height: 2px; 22 background: #ddd; 23} 24 25.timeline-item::after { 26 content: ''; 27 position: absolute; 28 top: 50%; 29 left: 50%; 30 transform: translate(-50%, -50%); 31 width: 12px; 32 height: 12px; 33 background: #3b82f6; 34 border-radius: 50%; 35}

Tab Panels with Scroll Snap#

1.tabs { 2 display: flex; 3 overflow-x: auto; 4 scroll-snap-type: x mandatory; 5} 6 7.tab-panel { 8 flex: 0 0 100%; 9 scroll-snap-align: start; 10 min-height: 200px; 11 padding: 1rem; 12} 13 14/* Sync tabs with scroll position */ 15.tab-buttons { 16 display: flex; 17 gap: 0.5rem; 18} 19 20.tab-button { 21 padding: 0.5rem 1rem; 22 border: none; 23 background: transparent; 24} 25 26.tab-button.active { 27 border-bottom: 2px solid #3b82f6; 28}

Scroll Snap with JavaScript#

1// Detect current snap item 2const container = document.querySelector('.scroll-container'); 3 4container.addEventListener('scrollend', () => { 5 const items = container.querySelectorAll('.item'); 6 const containerRect = container.getBoundingClientRect(); 7 8 items.forEach((item, index) => { 9 const itemRect = item.getBoundingClientRect(); 10 const isVisible = 11 itemRect.left >= containerRect.left && 12 itemRect.right <= containerRect.right; 13 14 if (isVisible) { 15 console.log('Current item:', index); 16 updateIndicators(index); 17 } 18 }); 19}); 20 21// Scroll to specific item 22function scrollToItem(index) { 23 const items = container.querySelectorAll('.item'); 24 items[index]?.scrollIntoView({ 25 behavior: 'smooth', 26 inline: 'start', 27 }); 28} 29 30// Navigation buttons 31document.querySelector('.next').addEventListener('click', () => { 32 container.scrollBy({ 33 left: container.clientWidth, 34 behavior: 'smooth', 35 }); 36}); 37 38document.querySelector('.prev').addEventListener('click', () => { 39 container.scrollBy({ 40 left: -container.clientWidth, 41 behavior: 'smooth', 42 }); 43});

Intersection Observer Integration#

1// Track visible items 2const observer = new IntersectionObserver( 3 (entries) => { 4 entries.forEach((entry) => { 5 if (entry.isIntersecting) { 6 entry.target.classList.add('visible'); 7 // Update pagination, analytics, etc. 8 } else { 9 entry.target.classList.remove('visible'); 10 } 11 }); 12 }, 13 { 14 root: document.querySelector('.scroll-container'), 15 threshold: 0.5, 16 } 17); 18 19document.querySelectorAll('.item').forEach((item) => { 20 observer.observe(item); 21});

Accessibility Considerations#

1<!-- Carousel with accessibility --> 2<div 3 class="carousel" 4 role="region" 5 aria-label="Image carousel" 6 tabindex="0" 7> 8 <div class="slide" role="group" aria-label="Slide 1 of 5"> 9 <img src="image1.jpg" alt="Description of image 1"> 10 </div> 11 <div class="slide" role="group" aria-label="Slide 2 of 5"> 12 <img src="image2.jpg" alt="Description of image 2"> 13 </div> 14</div> 15 16<!-- Navigation controls --> 17<div class="carousel-controls"> 18 <button aria-label="Previous slide">Previous</button> 19 <button aria-label="Next slide">Next</button> 20</div> 21 22<!-- Pagination indicators --> 23<div class="pagination" role="tablist"> 24 <button role="tab" aria-selected="true" aria-label="Go to slide 1">1</button> 25 <button role="tab" aria-selected="false" aria-label="Go to slide 2">2</button> 26</div>
1/* Focus styles */ 2.carousel:focus { 3 outline: 2px solid #3b82f6; 4 outline-offset: 2px; 5} 6 7/* Reduced motion */ 8@media (prefers-reduced-motion: reduce) { 9 .carousel { 10 scroll-behavior: auto; 11 } 12 13 .carousel-item { 14 scroll-snap-stop: always; 15 } 16}

Best Practices#

Implementation: ✓ Use mandatory for controlled UX ✓ Use proximity for flexible scrolling ✓ Add scroll-behavior: smooth ✓ Consider scroll-snap-stop: always Accessibility: ✓ Provide keyboard navigation ✓ Add ARIA labels ✓ Respect reduced motion ✓ Include visible focus states Performance: ✓ Use CSS-only when possible ✓ Debounce scroll event handlers ✓ Use Intersection Observer ✓ Avoid layout thrashing

Conclusion#

CSS Scroll Snap creates native-feeling scroll experiences without heavy JavaScript. Use it for carousels, galleries, and full-page sections. Combine with JavaScript for navigation controls and accessibility enhancements.

Share this article

Help spread the word about Bootspring