Scroll Snap

스크롤 스냅

Release a scroll and the nearest item snaps precisely into place — a CSS-only way to build galleries and carousels.

Also known as: Carousel snap
···
html
<div class="snap" id="snap">
  <div class="scard">1</div><div class="scard">2</div><div class="scard">3</div><div class="scard">4</div><div class="scard">5</div>
</div>
css
.fx-dot{position:absolute;left:0;top:0;width:12px;height:12px;margin:-6px 0 0 -6px;border-radius:50%;
  background:var(--accent);box-shadow:0 0 0 5px color-mix(in srgb,var(--accent) 18%,transparent),0 2px 6px rgba(0,0,0,.3);
  pointer-events:none;z-index:9999;transition:opacity .25s}
.snap{position:absolute;inset:0;display:flex;gap:12px;overflow-x:auto;overflow-y:hidden;
  scroll-snap-type:x mandatory;padding:0 40%;align-items:center;cursor:grab;scrollbar-width:none}
.snap::-webkit-scrollbar{display:none}
.scard{flex:0 0 auto;width:min(46%,120px);aspect-ratio:1/1;border-radius:16px;scroll-snap-align:center;
  display:grid;place-items:center;font-size:clamp(16px,5vmin,22px);font-weight:800;color:#fff;
  background:linear-gradient(135deg,var(--accent),var(--accent-2))}
.scard:nth-child(even){background:linear-gradient(135deg,var(--accent-3),var(--accent))}
js
const fxDot=document.createElement('div');fxDot.className='fx-dot';document.body.appendChild(fxDot);
let fxAuto=true,fxX=innerWidth/2,fxY=innerHeight/2;
function fxHide(){fxDot.style.opacity='0';}
addEventListener('pointerdown',()=>{fxAuto=false;fxHide();},{capture:true});
addEventListener('pointermove',e=>{fxAuto=false;fxHide();fxX=e.clientX;fxY=e.clientY;},{capture:true});
function fxWalk(x,y,ms){
  return new Promise(res=>{
    const sx=fxX,sy=fxY,start=performance.now();
    function step(now){
      if(!fxAuto)return res();
      const p=Math.min(1,(now-start)/ms),e=1-Math.pow(1-p,3);
      fxX=sx+(x-sx)*e;fxY=sy+(y-sy)*e;
      fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
      if(p<1)requestAnimationFrame(step);else res();
    }
    requestAnimationFrame(step);
  });
}
function fxWait(ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){ if(!fxAuto)return res(); if(now-start<ms)requestAnimationFrame(step);else res(); }
    requestAnimationFrame(step);
  });
}

const snap=document.getElementById('snap');
addEventListener('wheel',()=>{fxAuto=false;fxHide();},{capture:true,passive:true});
addEventListener('touchstart',()=>{fxAuto=false;fxHide();},{capture:true,passive:true});
let dragging=false, dragStartX=0, dragStartScroll=0;
snap.addEventListener('pointerdown',e=>{
  fxAuto=false; fxHide();
  dragging=true; dragStartX=e.clientX; dragStartScroll=snap.scrollLeft;
  snap.setPointerCapture(e.pointerId); snap.style.cursor='grabbing';
});
addEventListener('pointermove',e=>{ if(!dragging) return; snap.scrollLeft=dragStartScroll-(e.clientX-dragStartX); });
addEventListener('pointerup',()=>{ if(!dragging) return; dragging=false; snap.style.cursor='grab'; });
const cards=[...snap.querySelectorAll('.scard')];
async function autoSeq(){
  let i=0;
  while(fxAuto){
    await fxWait(1500); if(!fxAuto) break;
    i=(i+1)%cards.length;
    cards[i].scrollIntoView({behavior:'smooth', inline:'center', block:'nearest'});
    const r=snap.getBoundingClientRect();
    fxY=r.top+r.height/2;
    const steps=16;
    for(let s=0;s<=steps;s++){
      if(!fxAuto) break;
      fxX=r.left+r.width*(0.7-0.4*(s/steps));
      fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
      await new Promise(res=>requestAnimationFrame(res));
    }
  }
}
autoSeq();

Give the scroll container `scroll-snap-type: x mandatory` (horizontal) or `y mandatory` (vertical), and each item `scroll-snap-align: center` (or `start`). That's the whole API — the browser aligns wherever momentum scrolling ends to an item boundary.

So the first and last items can also reach the center of the viewport, add padding to the container roughly half an item's width on each side. Skip it and the edge items get stuck against the container's edge, looking out of alignment with the rest.

`mandatory` always comes to rest on a snap point; `proximity` only snaps when you land near one. If people flick through quickly a lot, `proximity` feels less rigid.

When to use

Use it for image galleries, onboarding slides, product carousels — any horizontal or vertical list meant to be viewed one item at a time.