Tailwind CSS Scroll Snap Stop
Scroll Snap Stop defines how strict or lenient the browser should be in snapping to specific scrollable positions. It is often used together with scroll-snap-type and scroll-snap-align to control the scrolling behavior. Tailwind provides snap-always
and snap-normal
utilities to apply scroll-snap-stop
CSS property.
In this guide, we'll explore how to use these utilities in Tailwind, starting from its basic application to advanced techniques such as conditional states and responsive implementations.
Class | Properties | Example |
---|---|---|
snap-normal | scroll-snap-stop: normal; | <div className="snap-normal"></div> |
snap-always | scroll-snap-stop: always; | <div className="snap-always"></div> |
Overview of Scroll Snap Stop
Adding the snap-always
The snap-always
utility enforces stricter snapping, ensuring the scroll always stops at the nearest snap point after interaction, providing more controlled navigation while still allowing intentional skips at high speeds.
export default function SnapStopDemo() { return ( <div className="snap-y snap-mandatory flex flex-col gap-4 items-center overflow-auto h-screen px-10 pt-10 bg-gray-50"> <div className="snap-always snap-center h-96"> <img src="https://images.unsplash.com/photo-1511467687858-23d96c32e4ae" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always snap-center h-96"> <img src="https://images.unsplash.com/photo-1487017159836-4e23ece2e4cf" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always snap-center h-96"> <img src="https://images.unsplash.com/photo-1508873699372-7aeab60b44ab" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always snap-center h-96"> <img src="https://images.unsplash.com/photo-1489269637500-aa0e75768394" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always snap-center h-96"> <img src="https://images.unsplash.com/photo-1488998427799-e3362cec87c3" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always snap-center h-96"> <img src="https://images.unsplash.com/photo-1527443224154-c4a3942d3acf" className="w-full h-full object-cover shadow-md" /> </div> </div> ); }
Adding the snap-normal
The snap-normal
utility allows for flexible scrolling where users can skip over multiple snap points if they scroll with enough velocity, making it ideal for fluid and less restrictive experiences.
export default function StrictSnap() { return ( <div className="snap-y snap-mandatory flex flex-col gap-4 items-center overflow-auto h-screen px-10 pt-10 bg-gray-50"> <div className="snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1511467687858-23d96c32e4ae" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1487017159836-4e23ece2e4cf" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1508873699372-7aeab60b44ab" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1489269637500-aa0e75768394" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1488998427799-e3362cec87c3" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1527443224154-c4a3942d3acf" className="w-full h-full object-cover shadow-md" /> </div> </div> ); }
States and Responsiveness
Scroll Snap Stop can also be used with Tailwind's state and responsive modifiers to modify the scroll-snap-stop
property based on certain states and screens.
Hover and Focus States
Use hover
and focus
modifiers to apply the Scroll Snap Stop on states like hover and focus, e.g., hover:snap-always
, etc.
export default function HoverSnapBehavior() { return ( <div className="snap-y snap-mandatory flex flex-col gap-4 items-center overflow-auto h-screen px-10 pt-10 bg-gray-50"> <div className="snap-always hover:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1511467687858-23d96c32e4ae" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always hover:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1487017159836-4e23ece2e4cf" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always hover:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1508873699372-7aeab60b44ab" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always hover:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1489269637500-aa0e75768394" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always hover:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1488998427799-e3362cec87c3" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always hover:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1527443224154-c4a3942d3acf" className="w-full h-full object-cover shadow-md" /> </div> </div> ); }
Breakpoint Modifiers
Use the breakpoint modifiers to apply Scroll Snap Stop on and above certain breakpoint, e.g., md:snap-always
, lg:snap-normal
, etc.
export default function ResponsiveSnap() { return ( <div className="snap-y snap-mandatory flex flex-col gap-4 items-center overflow-auto h-screen px-10 pt-10 bg-gray-50"> <div className="snap-always lg:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1511467687858-23d96c32e4ae" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always lg:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1487017159836-4e23ece2e4cf" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always lg:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1508873699372-7aeab60b44ab" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always lg:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1489269637500-aa0e75768394" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always lg:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1488998427799-e3362cec87c3" className="w-full h-full object-cover shadow-md" /> </div> <div className="snap-always lg:snap-normal snap-center h-96"> <img src="https://images.unsplash.com/photo-1527443224154-c4a3942d3acf" className="w-full h-full object-cover shadow-md" /> </div> </div> ); }
Real World Examples
Product Showcase Carousel
A horizontal product showcase with mandatory snap points, perfect for e-commerce featured items.
export default function ProductShowcase() { const products = [ { id: 1, name: "Premium Leather Bag", price: "$299", src: "https://images.unsplash.com/photo-1548036328-c9fa89d128fa", alt: "Brown leather messenger bag" }, { id: 2, name: "Vintage Watch", price: "$199", src: "https://images.unsplash.com/photo-1524592094714-0f0654e20314", alt: "Classic analog watch" }, { id: 3, name: "Designer Sunglasses", price: "$159", src: "https://images.unsplash.com/photo-1572635196237-14b3f281503f", alt: "Modern sunglasses" }, { id: 4, name: "Handcrafted Wallet", price: "$89", src: "https://images.unsplash.com/photo-1627123424574-724758594e93", alt: "Leather wallet" }, { id: 5, name: "Silver Necklace", price: "$129", src: "https://images.unsplash.com/photo-1599643478518-a784e5dc4c8f", alt: "Sterling silver necklace" }, { id: 6, name: "Canvas Backpack", price: "$79", src: "https://images.unsplash.com/photo-1553062407-98eeb64c6a62", alt: "Durable canvas backpack" } ]; return ( <div className="w-full overflow-x-scroll snap-x snap-mandatory"> <div className="flex"> {products.map((product) => ( <div key={product.id} className="w-80 flex-shrink-0 snap-start snap-always p-4"> <img src={product.src} alt={product.alt} className="w-full h-64 object-cover rounded-lg" /> <h3 className="mt-2 text-lg font-semibold">{product.name}</h3> <p className="text-gray-600">{product.price}</p> </div> ))} </div> </div> ); }
Vertical Story Viewer
A vertical story viewer with mandatory snap points, similar to social media stories.
export default function StoryViewer() { const stories = [ { id: 1, username: "traveler_jane", src: "https://images.unsplash.com/photo-1682687220866-c856f566f1bd", alt: "Sunset in Bali", caption: "Beautiful sunset in Bali" }, { id: 2, username: "foodie_john", src: "https://images.unsplash.com/photo-1682687221248-3116ba6ab483", alt: "Italian pasta dish", caption: "Homemade pasta in Rome" }, { id: 3, username: "adventure_sam", src: "https://images.unsplash.com/photo-1682687220063-4742bd7fd538", alt: "Mountain hiking", caption: "Hiking in the Alps" }, { id: 4, username: "nature_lover", src: "https://images.unsplash.com/photo-1682687220199-d0124f48f95b", alt: "Forest waterfall", caption: "Hidden waterfall discovered" }, ]; return ( <div className="h-screen overflow-y-scroll snap-y snap-mandatory"> {stories.map((story) => ( <div key={story.id} className="h-screen flex-shrink-0 snap-start snap-always relative"> <img src={story.src} alt={story.alt} className="w-full h-full object-cover" /> <div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/60"> <p className="text-white font-bold">{story.username}</p> <p className="text-white/80">{story.caption}</p> </div> </div> ))} </div> ); }
Portfolio Gallery Grid
A grid-based portfolio gallery with mandatory snap points for both horizontal and vertical scrolling.
export default function PortfolioGrid() { const projects = [ { id: 1, title: "Brand Identity", category: "Branding", src: "https://images.unsplash.com/photo-1614036634955-ae5e90f9b9eb", alt: "Brand identity project" }, { id: 2, title: "Web Design", category: "UI/UX", src: "https://images.unsplash.com/photo-1542744094-3a31f272c490", alt: "Web design project" }, { id: 3, title: "Mobile App", category: "Development", src: "https://images.unsplash.com/photo-1542641728-6ca359b085f4", alt: "Mobile app project" }, { id: 4, title: "Photography", category: "Art", src: "https://images.unsplash.com/photo-1542038784456-1ea8e935640e", alt: "Photography project" }, { id: 5, title: "Print Design", category: "Graphics", src: "https://images.unsplash.com/photo-1561070791-2526d30994b5", alt: "Print design project" }, { id: 6, title: "Motion Graphics", category: "Animation", src: "https://images.unsplash.com/photo-1627843240043-aa499ed215e7", alt: "Motion graphics project" } ]; return ( <div className="h-screen w-full overflow-scroll grid grid-cols-1 gap-4 p-4 snap-both snap-mandatory"> {projects.map((project) => ( <div key={project.id} className="aspect-square snap-start snap-always relative group" > <img src={project.src} alt={project.alt} className="w-full h-full object-cover" /> <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-center items-center text-white"> <h3 className="text-xl font-bold">{project.title}</h3> <p className="text-sm">{project.category}</p> </div> </div> ))} </div> ); }
Recipe Steps Slider
A horizontal recipe slider with mandatory snap points for better focus on each instruction.
export default function RecipeSteps() { const steps = [ { id: 1, title: "Prepare Ingredients", instruction: "Gather all ingredients and measure them accurately", src: "https://images.unsplash.com/photo-1506368083636-6defb67639a7", alt: "Cooking ingredients" }, { id: 2, title: "Mix Dry Ingredients", instruction: "Combine flour, sugar, and spices in a bowl", src: "https://images.unsplash.com/photo-1556911073-52527ac43761", alt: "Mixing ingredients" }, { id: 3, title: "Add Wet Ingredients", instruction: "Slowly incorporate eggs and milk", src: "https://images.unsplash.com/photo-1551185618-07fd482ff86e", alt: "Adding wet ingredients" }, { id: 4, title: "Knead Dough", instruction: "Knead until smooth and elastic", src: "https://images.unsplash.com/photo-1620567845971-d4d280176e68", alt: "Kneading dough" }, { id: 5, title: "Let it Rest", instruction: "Cover and let rest for 30 minutes", src: "https://images.unsplash.com/photo-1471646174523-327e108889e7", alt: "Resting dough" }, { id: 6, title: "Bake", instruction: "Bake at 350°F for 25 minutes", src: "https://images.unsplash.com/photo-1587248721300-d7a39ea29585", alt: "Baking process" } ]; return ( <div className="w-full overflow-x-scroll snap-x snap-mandatory"> <div className="flex"> {steps.map((step) => ( <div key={step.id} className="w-screen flex-shrink-0 snap-start snap-always p-8" > <div className="max-w-2xl mx-auto bg-white rounded-xl shadow-lg overflow-hidden"> <img src={step.src} alt={step.alt} className="w-full h-64 object-cover" /> <div className="p-6"> <div className="text-2xl font-bold mb-2">Step {step.id}: {step.title}</div> <p className="text-gray-600">{step.instruction}</p> </div> </div> </div> ))} </div> </div> ); }
Timeline Showcase
A vertical timeline with mandatory snap points for each major event or milestone.
export default function Timeline() { const events = [ { id: 1, year: "2018", title: "Company Founded", description: "Started in a small garage with big dreams", src: "https://images.unsplash.com/photo-1552650844-ad8bc948e1c6", alt: "Company founding" }, { id: 2, year: "2019", title: "First Major Client", description: "Landed our first enterprise contract", src: "https://images.unsplash.com/photo-1549923746-c502d488b3ea", alt: "Client meeting" }, { id: 3, year: "2020", title: "Office Expansion", description: "Moved to our first official headquarters", src: "https://images.unsplash.com/photo-1557804506-669a67965ba0", alt: "New office" }, { id: 4, year: "2021", title: "International Growth", description: "Expanded operations to Europe", src: "https://images.unsplash.com/photo-1511578314322-379afb476865", alt: "Global expansion" }, { id: 5, year: "2022", title: "Product Launch", description: "Released our flagship product", src: "https://images.unsplash.com/photo-1531058020387-3be344556be6", alt: "Product launch" }, { id: 6, year: "2023", title: "Industry Award", description: "Recognized as industry leader", src: "https://images.unsplash.com/photo-1594122230689-45899d9e6f69", alt: "Award ceremony" } ]; return ( <div className="h-screen overflow-y-scroll snap-y snap-mandatory"> {events.map((event) => ( <div key={event.id} className="h-screen flex-shrink-0 snap-start snap-always flex items-center justify-center relative" > <img src={event.src} alt={event.alt} className="absolute inset-0 w-full h-full object-cover" /> <div className="relative z-10 bg-white/90 p-8 rounded-xl max-w-xl mx-4"> <div className="text-5xl font-bold text-blue-600 mb-4">{event.year}</div> <h3 className="text-2xl font-bold mb-2">{event.title}</h3> <p className="text-gray-600">{event.description}</p> </div> </div> ))} </div> ); }
Best Practices
Optimize for Reusability
To improve reusability, design multiple components with scalable snap behaviors that can be used across different layouts. Defining reusable scroll-snap-stop
patterns ensures that design elements remain adaptable to various sections, reducing the need for excessive reconfiguration when adding new content.
Encapsulating snapping behaviors within reusable component structures helps maintain efficiency. For instance, a carousel component with scroll-snap-stop
as props
can be adapted to different contexts without requiring extensive modifications.
Build Responsive Design
Using scroll-snap-stop
responsively ensures a consistent user experience across devices of varying screen sizes. Tailwind’s responsive variants allow defining the snapping behavior by applying different snapping modes at various breakpoints.
For example, sm:snap-normal
, lg:snap-always
, etc., ensures an adaptive experience that caters to different screen sizes. Additionally, test interactions on both mobile and desktop to refine the snap behavior for optimal usability.
Accessibility Considerations
Enhance Readability and Navigability
Proper implementation of scroll-snap-stop
improves content readability and navigability by ensuring that key sections are brought fully into view. For example, applying the snap-always
utility on a critical section—like the header of an article or the main image in an interactive gallery—ensures that the element always snaps precisely to the viewport. This guarantees that users don't miss important content during scrolling.
On the other hand, using snap-normal
allows for a more relaxed snapping behavior. With snap-normal
, elements may not force a snap if the scrolling momentum is high, providing a smoother transition between sections. This is ideal for paginated layouts or long-form content where a less rigid scroll experience can reduce visual strain.
Focus on High Contrast
For users with visual impairments, proper contrast adjustments ensure that snapped content remains easily distinguishable. When using snap-always
and snap-normal
, it's important that these behaviors do not create abrupt visual changes that obscure text or create contrast inconsistencies.
For example, snap-always
forces an element to align exactly in the viewport, which might result in sudden shifts; in these cases, ensuring a high contrast ratio between foreground and background elements is essential for maintaining readability.
Tailwind’s utility classes like bg-opacity-*
and text-opacity-*
are helpful in these scenarios as they ensure that text and interactive elements remain clear even during snapping interactions.