Onboarding Carousel

온보딩 캐러셀

The 2–4 swipeable intro slides a first-time user sees, ending in a "Get Started" button on the last one.

Also known as: Intro slides앱 소개 슬라이드
···
html
<div class="stage"><div class="phone"><div class="notch"></div><div class="screen" id="screen">
<div class="obwrap">
  <div class="obtrack" id="obtrack">
    <div class="obslide"><div class="obicon i0"></div><p>Track your habits</p></div>
    <div class="obslide"><div class="obicon i1"></div><p>Get gentle reminders</p></div>
    <div class="obslide"><div class="obicon i2"></div><p>See your progress</p></div>
  </div>
</div>
<div class="obdots"><span class="active"></span><span></span><span></span></div>
<button class="obbtn" id="obbtn">Next</button></div><div class="home"></div></div></div>
css
.stage{position:absolute;inset:0;display:grid;place-items:center;overflow:hidden}
.phone{position:relative;height:90vmin;max-height:640px;aspect-ratio:9/19.5;background:#08080c;
  border-radius:20%/8.6%;box-shadow:0 0 0 2px rgba(255,255,255,.08) inset,0 12px 30px rgba(0,0,0,.35)}
.phone .notch{position:absolute;left:50%;top:0;transform:translateX(-50%);width:38%;height:5.6%;
  background:#08080c;border-radius:0 0 46% 46%;z-index:80}
.phone .screen{position:absolute;top:7.6%;left:3%;right:3%;bottom:3%;border-radius:11%/4.6%;overflow:hidden;
  background:var(--surface);color:var(--fg)}
.phone .home{position:absolute;left:50%;bottom:1.1%;transform:translateX(-50%);width:32%;height:3px;
  border-radius:2px;background:rgba(255,255,255,.45);z-index:80}
.pfx{position:absolute;left:0;top:0;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;
  background:color-mix(in srgb,var(--accent) 55%,transparent);
  box-shadow:0 0 0 6px color-mix(in srgb,var(--accent) 15%,transparent);pointer-events:none;z-index:999;
  transition:opacity .2s}
.pfx.hold{width:30px;height:30px;margin:-15px 0 0 -15px;
  box-shadow:0 0 0 9px color-mix(in srgb,var(--accent) 20%,transparent);
  transition:width .5s linear,height .5s linear,margin .5s linear,box-shadow .5s linear,opacity .2s}

.obwrap{position:absolute;inset:0 0 20%;overflow:hidden}
.obtrack{display:flex;height:100%;width:300%;touch-action:none}
.obslide{width:33.333%;display:grid;place-items:center;gap:8%;padding:10% 12%;text-align:center}
.obicon{width:clamp(24px,16vmin,46px);height:clamp(24px,16vmin,46px);border-radius:26%;background:var(--accent);opacity:.85}
.i1{border-radius:50%}
.i2{border-radius:50% 50% 50% 6%}
.obslide p{margin:0;font-weight:700;font-size:clamp(7px,2.6vmin,10px);color:var(--fg)}
.obdots{position:absolute;left:0;right:0;bottom:11%;display:flex;justify-content:center;gap:6px}
.obdots span{width:5px;height:5px;border-radius:50%;background:var(--line);transition:width .2s,background .2s}
.obdots span.active{background:var(--accent);width:14px;border-radius:3px}
.obbtn{position:absolute;left:8%;right:8%;bottom:3%;height:9%;border:none;border-radius:999px;background:var(--accent);
  color:#fff;font-weight:700;font-size:clamp(7px,2.6vmin,9.5px)}
js
const pfx=document.createElement('div');pfx.className='pfx';document.body.appendChild(pfx);
const screenEl=document.getElementById('screen');
let pfxAuto=true;
function pfxHide(){pfx.style.opacity='0';}
addEventListener('pointerdown',()=>{pfxAuto=false;pfxHide();},{capture:true});
function pfxAt(fx,fy){
  const r=screenEl.getBoundingClientRect();
  const x=r.left+r.width*fx, y=r.top+r.height*fy;
  pfx.style.transform='translate('+x+'px,'+y+'px)';
  return {x,y};
}
function pfxWalk(x0,y0,x1,y1,ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){
      if(!pfxAuto) return res();
      const p=Math.min(1,(now-start)/ms), e=1-Math.pow(1-p,3);
      pfxAt(x0+(x1-x0)*e, y0+(y1-y0)*e);
      if(p<1) requestAnimationFrame(step); else res();
    }
    requestAnimationFrame(step);
  });
}
function pfxWait(ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){ if(!pfxAuto) return res(); if(now-start<ms) requestAnimationFrame(step); else res(); }
    requestAnimationFrame(step);
  });
}

const track=document.getElementById('obtrack'), dots=[...document.querySelectorAll('.obdots span')], btn=document.getElementById('obbtn');
let idx=0;
function go(i){
  idx=Math.max(0,Math.min(2,i));
  track.style.transition='transform .35s ease';
  track.style.transform='translateX('+(-idx*33.333)+'%)';
  dots.forEach((d,j)=>d.classList.toggle('active',j===idx));
  btn.textContent= idx===2?'Get Started':'Next';
}
btn.addEventListener('pointerdown',()=>{ pfxAuto=false; pfxHide(); go(idx===2?0:idx+1); });
let dragging=false,startX=0;
track.addEventListener('pointerdown',e=>{ pfxAuto=false; pfxHide(); dragging=true; startX=e.clientX; track.style.transition='none'; });
addEventListener('pointermove',e=>{
  if(!dragging)return;
  track.style.transform='translateX(calc('+(-idx*33.333)+'% + '+(e.clientX-startX)+'px))';
});
addEventListener('pointerup',e=>{
  if(!dragging)return; dragging=false;
  const dx=e.clientX-startX;
  if(dx<-40) go(idx+1); else if(dx>40) go(idx-1); else go(idx);
});
go(0);
async function autoSeq(){
  while(pfxAuto){
    for(let i=0;i<3;i++){
      if(!pfxAuto)break;
      go(i);
      await pfxWalk(0.82,0.7,0.82,0.7,250); if(!pfxAuto)break;
      await pfxWait(650); if(!pfxAuto)break;
      if(i<2) await pfxWalk(0.82,0.7,0.18,0.7,380);
    }
  }
}
autoSeq();

The basic mechanism is one track of slides laid side by side, moved with `transform: translateX(-index * 100%)`. While dragging, add the raw finger movement directly for instant response; on release, use the distance and velocity to decide whether to advance, go back, or spring to rest.

The dot indicator isn't decoration — it's progress information, "slide N of total." Widening the dot for the current slide lets someone infer position even without a screen reader. Swapping the button label from "Next" to "Get Started" only on the final slide is an explicit signal that this is the end.

Onboarding isn't a mandatory gate to sit through every time — keep a "Skip" visible in a corner so anyone can leave whenever, and avoid mixing it with screens that collect input, like sign-up. There's also research suggesting feature explanations stick better as coach marks shown the first time someone actually meets that screen, rather than crammed into an intro carousel.

When to use

Use it on first launch to convey the app's core value in three slides or fewer. If there is a lot to explain, splitting it into in-app coach marks beats stretching the carousel longer.