CSS Scroll Snap
CSS Scroll Snap allows you to control scroll behavior by defining snap points — positions where the scrollport locks into place. This creates polished, app-like scrolling experiences for carousels, image galleries, section-by-section pages, and horizontal timelines.
Scroll Snap is entirely CSS-driven, requiring no JavaScript for the snap logic. It works on both horizontal and vertical scroll containers and provides fine-grained control over snap alignment and stopping behavior.
| 1 | .scroll-container { |
| 2 | scroll-snap-type: x mandatory; |
| 3 | overflow-x: auto; |
| 4 | display: flex; |
| 5 | gap: 16px; |
| 6 | } |
| 7 | |
| 8 | .scroll-item { |
| 9 | scroll-snap-align: start; |
| 10 | flex: 0 0 300px; |
| 11 | } |
The scroll-snap-type property is set on the scroll container. It defines the axis and snap strictness: whether snapping is required (mandatory) or suggested (proximity).
| 1 | .container { |
| 2 | /* Axis */ |
| 3 | scroll-snap-type: none; /* default — no snapping */ |
| 4 | scroll-snap-type: x; /* horizontal snapping */ |
| 5 | scroll-snap-type: y; /* vertical snapping */ |
| 6 | scroll-snap-type: both; /* both axes */ |
| 7 | scroll-snap-type: block; /* block axis (respects writing mode) */ |
| 8 | scroll-snap-type: inline; /* inline axis */ |
| 9 | |
| 10 | /* Strictness */ |
| 11 | scroll-snap-type: x mandatory; /* must snap to the nearest point */ |
| 12 | scroll-snap-type: y proximity; /* snaps only when close enough */ |
| 13 | scroll-snap-type: both mandatory; |
| 14 | } |
warning
Set on child elements, scroll-snap-align defines where the element snaps to within the scroll container — at its start, center, or end.
| 1 | .item { |
| 2 | scroll-snap-align: start; /* snaps to the start of the container */ |
| 3 | scroll-snap-align: center; /* centers in the container */ |
| 4 | scroll-snap-align: end; /* snaps to the end of the container */ |
| 5 | scroll-snap-align: none; /* excluded from snapping */ |
| 6 | } |
| 7 | |
| 8 | /* Different items can have different alignment */ |
| 9 | .item:first-child { scroll-snap-align: start; } |
| 10 | .item:last-child { scroll-snap-align: end; } |
| 11 | .item:not(:first-child):not(:last-child) { scroll-snap-align: center; } |
Controls whether the scroll container is allowed to pass over snap positions during a fast scroll gesture. By default (normal), fast scrolls can skip items. Setting always forces the scroll to stop at every snap position.
| 1 | .item { |
| 2 | scroll-snap-stop: normal; /* default — can skip during fast scrolls */ |
| 3 | scroll-snap-stop: always; /* force stop at this snap position */ |
| 4 | } |
info
scroll-margin adds space around a snap target, pushing the snap position inward. This is useful for creating visual breathing room or accounting for fixed headers.
| 1 | .item { |
| 2 | scroll-snap-align: start; |
| 3 | scroll-margin: 20px; /* 20px inset on all sides */ |
| 4 | scroll-margin-top: 60px; /* account for fixed header */ |
| 5 | scroll-margin-inline: 24px; /* logical property */ |
| 6 | } |
scroll-padding is set on the scroll container and defines the offset that snap positions must respect. This is ideal for ensuring content is visible past fixed headers or toolbars.
| 1 | .scroll-container { |
| 2 | scroll-snap-type: y mandatory; |
| 3 | scroll-padding-top: 80px; /* snap starts 80px below top */ |
| 4 | scroll-padding: 80px 0 0 0; /* shorthand */ |
| 5 | scroll-padding-block-start: 80px; /* logical property */ |
| 6 | } |
| 7 | |
| 8 | /* With sticky header — content snaps below the header */ |
| 9 | .header { |
| 10 | position: sticky; |
| 11 | top: 0; |
| 12 | height: 60px; |
| 13 | } |
| 14 | |
| 15 | .main { |
| 16 | scroll-snap-type: y mandatory; |
| 17 | scroll-padding-top: 60px; |
| 18 | } |
| 19 | |
| 20 | .section { |
| 21 | scroll-snap-align: start; |
| 22 | min-height: 100vh; |
| 23 | } |
Scroll Snap works on both axes. The patterns differ slightly based on the layout direction.
Horizontal Snap (Carousel)
| 1 | .carousel { |
| 2 | display: flex; |
| 3 | overflow-x: auto; |
| 4 | scroll-snap-type: x mandatory; |
| 5 | gap: 16px; |
| 6 | scroll-padding: 0 24px; |
| 7 | } |
| 8 | |
| 9 | .carousel-item { |
| 10 | flex: 0 0 80%; |
| 11 | scroll-snap-align: start; |
| 12 | max-width: 400px; |
| 13 | } |
Vertical Snap (Full Page)
| 1 | .fullpage { |
| 2 | height: 100vh; |
| 3 | overflow-y: auto; |
| 4 | scroll-snap-type: y mandatory; |
| 5 | scroll-snap-stop: always; |
| 6 | } |
| 7 | |
| 8 | .fullpage-section { |
| 9 | height: 100vh; |
| 10 | scroll-snap-align: start; |
| 11 | display: flex; |
| 12 | align-items: center; |
| 13 | justify-content: center; |
| 14 | } |
The strictness value determines how aggressively the scrollport snaps to positions.
| Value | Behavior | Best For |
|---|---|---|
| mandatory | Always snaps to the nearest snap position when scrolling stops | Carousels, galleries, step-by-step wizards |
| proximity | Only snaps if the scroll position is close enough to a snap point | Long pages, scrolling lists, content that needs free scrolling |
note
Image Carousel
| 1 | .carousel { |
| 2 | display: flex; |
| 3 | overflow-x: auto; |
| 4 | scroll-snap-type: x mandatory; |
| 5 | scroll-behavior: smooth; |
| 6 | -webkit-overflow-scrolling: touch; |
| 7 | gap: 8px; |
| 8 | padding: 16px; |
| 9 | } |
| 10 | |
| 11 | .carousel img { |
| 12 | flex: 0 0 100%; |
| 13 | scroll-snap-align: center; |
| 14 | object-fit: cover; |
| 15 | border-radius: 8px; |
| 16 | } |
| 17 | |
| 18 | /* Hide scrollbar */ |
| 19 | .carousel { |
| 20 | scrollbar-width: none; |
| 21 | } |
| 22 | .carousel::-webkit-scrollbar { display: none; } |
Section-by-Section Scrolling
| 1 | .site-container { |
| 2 | height: 100vh; |
| 3 | overflow-y: auto; |
| 4 | scroll-snap-type: y mandatory; |
| 5 | scroll-snap-stop: always; |
| 6 | } |
| 7 | |
| 8 | .section { |
| 9 | height: 100vh; |
| 10 | scroll-snap-align: start; |
| 11 | display: flex; |
| 12 | flex-direction: column; |
| 13 | justify-content: center; |
| 14 | padding: 80px; |
| 15 | } |
| 16 | |
| 17 | /* Progress indicator for snap sections */ |
| 18 | .section::after { |
| 19 | content: ''; |
| 20 | position: fixed; |
| 21 | right: 24px; |
| 22 | top: 50%; |
| 23 | transform: translateY(-50%); |
| 24 | width: 8px; |
| 25 | height: 8px; |
| 26 | border-radius: 50%; |
| 27 | background: #00FF41; |
| 28 | opacity: 0.3; |
| 29 | } |
| 30 | |
| 31 | .section:nth-child(1):target::after { opacity: 1; } |
Terminal-Like Paging
| 1 | .terminal-output { |
| 2 | height: 80vh; |
| 3 | overflow-y: auto; |
| 4 | scroll-snap-type: y proximity; |
| 5 | background: #0D0D0D; |
| 6 | border: 1px solid #222222; |
| 7 | border-radius: 8px; |
| 8 | font-family: 'JetBrains Mono', monospace; |
| 9 | font-size: 13px; |
| 10 | } |
| 11 | |
| 12 | .log-line { |
| 13 | scroll-snap-align: start; |
| 14 | padding: 4px 16px; |
| 15 | border-bottom: 1px solid #1A1A1A; |
| 16 | } |
| 17 | |
| 18 | .log-line:hover { |
| 19 | background: #1A1A2E; |
| 20 | } |
| Browser | Support | Version |
|---|---|---|
| Chrome | ✓ | 69+ |
| Firefox | ✓ | 68+ |
| Safari | ✓ | 11+ |
| Edge | ✓ | 79+ |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.