GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

캐러셀 슬라이드 예제 9종 — 관성 드래그부터 3D 원근까지

캐러셀 슬라이드 9종 가운데 하나는 카드가 넘어갈 때마다 관성으로 조금 더 미끄러진 뒤 멈추고, 하나는 끝에 닿을 때마다 고무줄처럼 살짝 튕겨 돌아오며, 하나는 카드가 3D 원근으로 둥글게 휘어지며 옆으로 지나갑니다.

아홉 항목 모두 카드를 이전·다음 버튼과 화살표 키로 넘길 수 있는 공통 골격 위에서, 항목마다 실제 입력 방식(포인터 드래그·휠·마우스 오버)과 계산 한 겹만 다르게 얹었습니다. 01·02·03·05·06·07·09 는 requestAnimationFrame 이 전환을 프레임마다 계산하고, 04 는 CSS transition 이 전환을 맡고 transitionend 시점에만 위치를 감쪽같이 바꾸며, 08 은 wheel 값을 받는 즉시 반영해 둘 다 rAF 루프가 없습니다. Math.random 은 아홉 항목 어디에도 없어 같은 조작을 반복하면 항상 같은 결과가 나옵니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 가지 전환 — 계산 한 겹이 어디서 도는가

아홉 항목은 하나의 카드 배열을 감싸는 같은 마크업 위에서 계산 방식만 바꿔 얹은 세트입니다. 그래서 캐러셀 슬라이드 만들기를 매번 새로 짜지 않고, 같은 좌우 이동 골격에 관성·고무줄·원근·모듈로 래핑·자동재생·중앙 정렬·스냅·깊이 스택·크로스페이드 가운데 계산 한 겹만 바꿔 끼웠습니다.

항목 누적면적 강도_평균 움직인프레임
01 드래그를 놓으면 관성으로 더 미끄러지는 캐러셀 27.17% 174.3 23/23
02 끝에 닿으면 고무줄처럼 튕겨 돌아오는 캐러셀 34.82% 132.2 23/23
03 카드가 3D 원근으로 휘어지며 지나가는 캐러셀 19.65% 76.6 22/23
04 끝까지 가면 처음으로 이어붙는 무한 루프 캐러셀 34.82% 145.9 9/23
05 자동으로 넘어가다 마우스를 올리면 멈추는 캐러셀 34.83% 108.8 20/23
06 아래 썸네일이 현재 카드에 맞춰 밝아지는 캐러셀 33.85% 55.5 22/23
07 손을 놓으면 항상 카드 한 장에 딱 맞춰 멈추는 캐러셀 34.80% 129.1 18/23
08 카드가 위아래로 쌓이며 넘어가는 세로 캐러셀 40.39% 84.2 18/23
09 카드가 흐려지며 깊이로 물러나는 크로스페이드 캐러셀 34.82% 58.0 22/23

누적면적이 가장 큰 항목은 08 로 40.39%인데, 카드가 위아래로 겹쳐 쌓이며 넘어가 화면 세로 전체가 함께 움직이기 때문입니다. 가장 작은 항목은 03 으로 19.65%인데, 가운데 카드만 크게 서고 나머지는 작게 물러나 있어 프레임마다 바뀌는 픽셀이 상대적으로 적습니다. 강도_평균이 가장 높은 항목은 01 하나로 174.3까지 오르는데, 손을 놓은 뒤에도 카드가 계속 빠르게 미끄러져 프레임 사이 픽셀 차가 큽니다. 가장 낮은 항목은 06 으로 55.5인데, 작은 썸네일 띠 한 줄만 살짝 움직이기 때문입니다. 23개 델타 구간 가운데 움직인프레임이 가장 촘촘한 항목은 01·02 로 나란히 23/23 이고, 가장 뜸한 항목은 04 로 9/23 입니다.

관성 감쇠 루프가 pos 에 속도를 더하고 마찰로 줄인다 — 01

settle 은 매 프레임 pos += velocity*dt 로 위치를 옮기고 velocity *= (1-MOMENTUM_FRICTION)^(dt/16.6667) 로 속도를 줄여, 손을 놓은 순간의 속도가 남아 있는 동안만 카드가 계속 미끄러집니다.

경계를 넘은 거리를 점근 곡선으로 눌러 고무줄처럼 보이게 한다 — 02

rubber 는 경계를 넘은 거리를 RUBBER_BAND_RANGE 에 점근하는 곡선으로 눌러 무한정 늘어나지 않게 막고, springBackIfNeeded 가 SPRING_STIFFNESS·SPRING_DAMPING 으로 목표(가까운 경계)까지 당겨 돌아옵니다.

인덱스 거리로 회전각·깊이·크기·투명도를 동시에 계산한다 — 03

render 는 각 카드와 중심 인덱스 사이의 거리(소수 가능)를 ANGLE_STEP·DEPTH_STEP·SCALE_STEP·OPACITY_STEP 네 계수에 곱해 rotateY·translateZ·scale·opacity 를 한 번에 구합니다.

클론 카드와 모듈로 판정으로 경계를 감쪽같이 넘긴다 — 04

맨 앞뒤에 이웃 카드를 CLONE_COUNT 만큼 미리 복제해 두고, wrapIfNeeded 가 위치가 복제 구간에 닿은 순간 transition 을 끈 채 같은 그림의 실제 칸으로 pos 를 옮겨 onTransitionEnd 에서 이 판정을 부릅니다.

남은 시간을 보존한 채 진행 막대를 채운다 — 05

tick 은 playing·hovering·reduce 가 모두 조건에 맞을 때만 경과 시간을 더하고, 그 값을 AUTOPLAY_MS 로 나눠 진행 막대 폭에 그대로 씁니다 — 커서가 머무는 동안은 더하지 않아 다시 풀렸을 때 처음부터가 아니라 멈췄던 지점부터 이어집니다.

활성 썸네일 중심을 띠 중심으로 매 프레임 보간한다 — 06

computeTargetOffset 이 활성 썸네일의 중심이 띠 중심에 오도록 목표 오프셋을 산술로 구하고, centerActiveThumb 이 CENTER_EASE 비율만큼 매 프레임 그 값으로 다가갑니다 — 브라우저 기본 scrollIntoView 를 쓰지 않습니다.

놓는 속도가 기준을 넘으면 다음 칸으로 스냅 판정을 밀어 넘긴다 — 07

judgeSnapIndex 는 그냥 가장 가까운 칸이 아니라, 놓는 순간의 속도가 SNAP_VELOCITY_BIAS 를 넘으면 그 방향의 다음 칸으로 목표를 밀어 넘기고, animateTo 가 SNAP_EASE 비율로 그 칸까지 당겨 붙입니다.

깊이 값을 translateY·scale·opacity·z-index 네 값에 동시에 매핑한다 — 08

render 는 쌓인 순서(depth)마다 STACK_STEP_Y·STACK_SCALE_STEP·STACK_OPACITY_STEP 로 translateY·scale·opacity 를 바닥값까지 수렴시키고, zIndex 를 TOTAL - depth 로 겹치는 순서를 맞춥니다.

삼차 이징 하나로 opacity·scale·translateZ 세 값을 함께 굽힌다 — 09

easeInOutCubic 이 매 프레임 진행률 p 를 굽히면 그 값 하나를 opacity·scale·translateZ 세 스타일에 동시에 써 앞 카드는 물러나고 뒤 카드는 다가오는 크로스페이드를 만듭니다 — paintStatic 은 전환이 없을 때 같은 최종 상태를 즉시 그립니다.

패턴별 코드

아래 아홉 조각은 각 항목의 실제 소스 파일에서 연속된 부분만 그대로 옮겼고, 줄 순서와 들여쓰기를 바꾸지 않았습니다.

01드래그를 놓으면 관성으로 더 미끄러지는 캐러셀

카드를 옆으로 밀었다 손을 떼면 놓은 속도만큼 카드가 더 미끄러지다가 점점 느려지며 멈추고, 세게 밀수록 더 멀리까지 넘어갑니다. 터치·마우스 드래그로 넘기는 제품 이미지 캐러셀에 맞습니다.

  // 관성 감쇠 루프 — 매 프레임 pos += velocity*dt, velocity *= (1-MOMENTUM_FRICTION)^(dt/16.6667).
  // 경계에 닿으면 그 자리에서 속도를 0으로 끊는다(되튕김은 이 항목의 역할이 아니다 — 02가 담당).
  function settle() {
    var last = performance.now();
    function frame(now) {
      var dt = Math.min(40, now - last);
      last = now;
      pos = clamp(pos + velocity * dt, minX, maxX);
      var decay = Math.pow(1 - MOMENTUM_FRICTION, dt / STEP_MS);
      velocity *= decay;
      if (pos === minX || pos === maxX) velocity = 0;
      paint();
      if (Math.abs(velocity) > VELOCITY_STOP) {
        settleFrame = requestAnimationFrame(frame);
      } else {
        velocity = 0;
        settleFrame = null;
      }
    }
    settleFrame = requestAnimationFrame(frame);
  }
드래그를 놓으면 관성으로 더 미끄러지는 캐러셀
새 창에서 열기

01 · 드래그를 놓으면 관성으로 더 미끄러지는 캐러셀 — 측정 누적면적 27.174% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

onPointerMoverequestAnimationFramereleaseVelocity

02끝에 닿으면 고무줄처럼 튕겨 돌아오는 캐러셀

첫 장이나 마지막 장에서 더 밀면 카드가 살짝 늘어나듯 따라가다가 손을 놓는 순간 고무줄처럼 튕겨 제자리로 돌아옵니다. 더 넘길 페이지가 없음을 손끝으로 바로 알리는 갤러리에 맞습니다.

  // 스프링 복귀 — pos 가 경계를 넘은 채로 있을 때만 목표(가까운 경계)까지 힘·감쇠로 당겨 돌아온다.
  function springBackIfNeeded() {
    var target = pos > maxX ? maxX : (pos < minX ? minX : null);
    if (target === null) return;
    var velocity = 0;
    function frame() {
      var force = (target - pos) * SPRING_STIFFNESS;
      velocity = (velocity + force) * SPRING_DAMPING;
      pos += velocity;
      paint();
      if (Math.abs(velocity) > SPRING_STOP || Math.abs(target - pos) > SPRING_STOP) {
        springFrame = requestAnimationFrame(frame);
      } else {
        pos = target;
        paint();
        springFrame = null;
      }
    }
    springFrame = requestAnimationFrame(frame);
  }
끝에 닿으면 고무줄처럼 튕겨 돌아오는 캐러셀
새 창에서 열기

02 · 끝에 닿으면 고무줄처럼 튕겨 돌아오는 캐러셀 — 측정 누적면적 34.824% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rawToVisualspringBackIfNeededonPointerMove

03카드가 3D 원근으로 휘어지며 지나가는 캐러셀

가운데 카드는 정면으로 크게 서고 양옆 카드는 각도가 기울어진 채 작게 물러나, 카드 줄 전체가 완만한 곡선을 그리며 회전합니다. 포트폴리오·앨범 표지를 훑어보는 화면에 맞습니다.

  // 계산 겹 — 카드마다 중심(index)까지의 거리(소수 가능)를 회전각·깊이·크기·투명도로 매핑한다.
  function render() {
    for (var n = 0; n < cards.length; n++) {
      var dist = n - index;
      var absDist = Math.abs(dist);
      var angle = clamp(dist * ANGLE_STEP, -MAX_ANGLE, MAX_ANGLE);
      var z = -absDist * DEPTH_STEP;
      var scale = Math.max(SCALE_FLOOR, 1 - absDist * SCALE_STEP);
      var opacity = Math.max(OPACITY_FLOOR, 1 - absDist * OPACITY_STEP);
      var x = dist * 58;
      cards[n].style.transform = 'translateX(' + x.toFixed(1) + 'px) translateZ(' + z.toFixed(1) + 'px) rotateY(' + angle.toFixed(1) + 'deg) scale(' + scale.toFixed(3) + ')';
      cards[n].style.opacity = opacity.toFixed(2);
      cards[n].style.zIndex = String(100 - Math.round(absDist * 10));
    }
    var shown = clamp(Math.round(index), 0, TOTAL - 1);
    readout.textContent = LABEL_PREFIX + (shown + 1) + LABEL_SEP + TOTAL;
  }
카드가 3D 원근으로 휘어지며 지나가는 캐러셀
새 창에서 열기

03 · 카드가 3D 원근으로 휘어지며 지나가는 캐러셀 — 측정 누적면적 19.651% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

animateIndexTotranslateZrotateY

04끝까지 가면 처음으로 이어붙는 무한 루프 캐러셀

마지막 카드 다음에 첫 카드의 복사본을 미리 이어 붙여 두어, 경계를 넘는 순간 눈치채지 못하게 처음 위치로 자리를 바꿔 끊김 없이 계속 돕니다. 브랜드 로고를 끊김 없이 계속 흘려보내는 띠에 맞습니다.

  // 모듈로 감기 — 위치가 복제 구간(첫 칸 미만 또는 마지막 칸 초과)에 닿으면, 전환이 끝난 그 순간
  // transition 을 끄고 같은 그림의 실제 칸으로 pos 만 순간 이동시킨다(화면상 티가 나지 않는다).
  function wrapIfNeeded() {
    if (pos < CLONE_COUNT) {
      pos += TOTAL;
      paint(false);
      void track.offsetWidth; // 강제 리플로우로 transition:none 을 확정시킨다
    } else if (pos >= CLONE_COUNT + TOTAL) {
      pos -= TOTAL;
      paint(false);
      void track.offsetWidth;
    }
  }

  function onTransitionEnd() {
    transitioning = false;
    wrapIfNeeded();
  }
끝까지 가면 처음으로 이어붙는 무한 루프 캐러셀
새 창에서 열기

04 · 끝까지 가면 처음으로 이어붙는 무한 루프 캐러셀 — 측정 누적면적 34.824% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

wrapIfNeededonTransitionEndrealIndex

05자동으로 넘어가다 마우스를 올리면 멈추는 캐러셀

일정 시간마다 다음 카드로 저절로 넘어가고 그 시간만큼 얇은 진행 막대가 차오르며, 카드 위에 커서가 머무는 동안에는 진행이 그대로 멈춥니다. 메인 배너처럼 사람이 없어도 계속 도는 홍보 영역에 맞습니다.

  // 계산 겹 — 매 프레임 dt 를 elapsed 에 더하되, hovering(또는 !playing) 이면 더하지 않는다.
  // 그래서 다시 풀렸을 때 처음부터가 아니라 '멈췄던 그 지점'부터 이어서 채워진다(남은 시간 보존).
  function tick(now) {
    if (!lastTick) lastTick = now;
    var dt = Math.min(48, now - lastTick);
    lastTick = now;
    if (playing && !hovering && !reduce) {
      elapsed += dt;
      if (elapsed >= AUTOPLAY_MS) { advance(); }
    }
    progressFill.style.width = (Math.min(1, elapsed / AUTOPLAY_MS) * 100).toFixed(1) + '%';
    rafId = requestAnimationFrame(tick);
  }
자동으로 넘어가다 마우스를 올리면 멈추는 캐러셀
새 창에서 열기

05 · 자동으로 넘어가다 마우스를 올리면 멈추는 캐러셀 — 측정 누적면적 34.826% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

showSlideonMouseEnterprogressFill

06아래 썸네일이 현재 카드에 맞춰 밝아지는 캐러셀

큰 화면의 카드가 바뀔 때마다 아래 작은 썸네일 줄에서 그 카드에 해당하는 칸만 테두리가 밝아지고 나머지는 옅게 가라앉아, 지금 몇 번째인지 한눈에 보입니다. 상품 상세 페이지의 큰 이미지와 썸네일 목록에 맞습니다.

  function centerActiveThumb() {
    targetOffset = computeTargetOffset();
    stopCenter();
    function frame() {
      offset += (targetOffset - offset) * CENTER_EASE;
      thumbTrack.style.transform = 'translate3d(' + offset.toFixed(1) + 'px,0,0)';
      if (Math.abs(targetOffset - offset) > CENTER_STOP) {
        centerFrame = requestAnimationFrame(frame);
      } else {
        offset = targetOffset;
        thumbTrack.style.transform = 'translate3d(' + offset.toFixed(1) + 'px,0,0)';
        centerFrame = null;
      }
    }
    centerFrame = requestAnimationFrame(frame);
  }
아래 썸네일이 현재 카드에 맞춰 밝아지는 캐러셀
새 창에서 열기

06 · 아래 썸네일이 현재 카드에 맞춰 밝아지는 캐러셀 — 측정 누적면적 33.855% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

computeTargetOffsetcenterActiveThumbthumbCenter

07손을 놓으면 항상 카드 한 장에 딱 맞춰 멈추는 캐러셀

손을 놓은 지점이 카드 사이 어중간한 자리여도 가장 가까운 카드 경계까지 부드럽게 미끄러져 항상 카드 한 장이 화면에 딱 맞게 정렬된 채로 멈춥니다. 카드 절반만 걸쳐 보이면 안 되는 정렬형 슬라이드쇼에 맞습니다.

  // 계산 겹 — 그냥 가장 가까운 칸이 아니라, 놓는 순간의 속도가 기준(SNAP_VELOCITY_BIAS)을 넘으면
  // '가까운 칸' 판정을 그 방향의 다음 칸으로 밀어 넘긴다(빠르게 짧게 밀어도 다음 칸으로 붙는다).
  function judgeSnapIndex(rawPos, velocity) {
    var rawIndex = -rawPos / step;
    var idx = Math.round(rawIndex);
    if (Math.abs(velocity) > SNAP_VELOCITY_BIAS) {
      idx += velocity < 0 ? 1 : -1;
    }
    return clamp(idx, 0, TOTAL - 1);
  }
손을 놓으면 항상 카드 한 장에 딱 맞춰 멈추는 캐러셀
새 창에서 열기

07 · 손을 놓으면 항상 카드 한 장에 딱 맞춰 멈추는 캐러셀 — 측정 누적면적 34.805% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

judgeSnapIndexreleaseVelocityanimateTo

08카드가 위아래로 쌓이며 넘어가는 세로 캐러셀

카드가 좌우가 아니라 위아래로 겹쳐 쌓이며 넘어가고, 다음 카드가 아래에서 올라와 앞 카드를 살짝 덮으며 자리를 대신합니다. 세로로 긴 모바일 화면의 스토리·공지 카드에 맞습니다.

  // 계산 겹 — 깊이 d 를 translateY·scale·opacity·z-index 로 매핑한다(선형이 아니라 바닥값으로 수렴).
  function render() {
    order.forEach(function (cardIdx, depth) {
      var el = cards[cardIdx];
      var y = depth * STACK_STEP_Y;
      var scale = Math.max(STACK_SCALE_FLOOR, 1 - depth * STACK_SCALE_STEP);
      var opacity = Math.max(STACK_OPACITY_FLOOR, 1 - depth * STACK_OPACITY_STEP);
      el.style.transform = 'translate3d(0,' + y.toFixed(1) + 'px,0) scale(' + scale.toFixed(3) + ')';
      el.style.opacity = opacity.toFixed(2);
      el.style.zIndex = String(TOTAL - depth);
    });
    readout.textContent = LABEL_PREFIX + (order[0] + 1) + LABEL_SEP + TOTAL;
  }
카드가 위아래로 쌓이며 넘어가는 세로 캐러셀
새 창에서 열기

08 · 카드가 위아래로 쌓이며 넘어가는 세로 캐러셀 — 측정 누적면적 40.394% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

onWheelonCardClickzIndex

09카드가 흐려지며 깊이로 물러나는 크로스페이드 캐러셀

다음 카드가 선명하게 떠오르는 동안 앞 카드는 옅어지면서 살짝 작아져 뒤로 물러나는 것처럼 보이고, 자리 이동 없이 그 위치에서 겹쳐 바뀝니다. 움직임을 최소화하고 싶은 잔잔한 배경 슬라이드에 맞습니다.

  // 이징 겹 — CSS 트랜지션이 아니라 매 프레임 p 를 직접 삼차 이징으로 굽혀
  // opacity·scale·translateZ 세 값을 동시에 계산한다.
  function easeInOutCubic(p) {
    return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
  }

  function paintStatic() {
    cards.forEach(function (el, n) {
      var active = n === current;
      el.style.opacity = active ? '1' : '0';
      el.style.transform = active ? 'translateZ(0) scale(1)' : 'translateZ(' + (-DEPTH_PUSH) + 'px) scale(' + (1 - DEPTH_SCALE).toFixed(3) + ')';
      el.style.zIndex = active ? '2' : '1';
    });
    dots.forEach(function (el, n) { el.classList.toggle('isActive', n === current); });
    readout.textContent = LABEL_PREFIX + (current + 1) + LABEL_SEP + TOTAL;
  }
카드가 흐려지며 깊이로 물러나는 크로스페이드 캐러셀
새 창에서 열기

09 · 카드가 흐려지며 깊이로 물러나는 크로스페이드 캐러셀 — 측정 누적면적 34.82% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

easeInOutCubictranslateZpaintStatic

값이 UI를 바꾸는 곳 — 관성 감쇠·튕김 강도·자동재생 간격

상수 하나만 바꿔도 카드가 움직이는 체감은 크게 달라집니다. 01 은 MOMENTUM_FRICTION(0.10)이 관성이 잦아드는 속도를 정합니다. 02 는 RUBBER_BAND_RANGE(64px)가 튕기는 최대 저항 폭을, SPRING_STIFFNESS(0.22)·SPRING_DAMPING(0.72)이 튕겨 돌아오는 세기와 잦아드는 정도를 정합니다. 03 은 ANGLE_STEP(38deg)이 카드 사이 회전각 간격을, DEPTH_STEP(90px)이 물러나는 깊이를 정합니다. 04 는 TRANSITION_MS(360ms)가 카드 한 칸 넘어가는 전환 길이를 정합니다. 05 는 AUTOPLAY_MS(1350ms)가 카드 한 장이 머무는 시간을 정합니다. 06 은 CENTER_EASE(0.24)가 썸네일이 중앙으로 당겨지는 속도를 정합니다. 07 은 SNAP_EASE(0.24)가 스냅 정렬 속도를, SNAP_VELOCITY_BIAS(0.32px/ms)가 다음 칸으로 넘기는 속도 기준을 정합니다. 08 은 STACK_STEP_Y(14px)가 카드 사이 세로 간격을 정합니다. 09 는 CROSSFADE_MS(480ms)가 크로스페이드 길이를, DEPTH_PUSH(70px)가 물러나는 깊이를 정합니다.

이웃 가지

캐러셀 축 안에서도 03·04·06 은 이웃 편과 이름이 겹치지만 계산 겹이 다릅니다. CSS 3D 큐브 캐러셀 틸트 9종 — 한 줄 복붙으로 와 082 는 모두 회전 카드 축을 다루지만, 03 은 포인터 드래그 위치를 매 프레임 각도·깊이·불투명도로 실시간 산술 매핑하고 animateIndexTo 로 스냅하는 JS 계산형이라 CSS scroll-snap 만으로 도는 043 의 큐브 틸트와 다릅니다. 이미지 슬라이드 css 9종 — GSAP 없이 반응형까지 의 썸네일 동기화는 active 클래스만 토글할 뿐 중앙 정렬 계산이 없는 반면, 이 편 06 은 computeTargetOffset 으로 목표 오프셋을 구하고 매 프레임 보간해 항상 중앙에 오도록 당기는 계산 겹이 하나 더 있습니다. 04 의 클론 카드 + 모듈로 래핑 조합은 이미지 비교 슬라이더 UI 9종 — 드래그하면 바로 보이는 을 포함한 이웃 편 어디에도 없는 방식입니다.

어디서 깨지는가 — 접근성·reduced-motion·터치 스와이프 충돌

위 아홉 카드 각각의 전체 vanilla 코드는 압축 파일로 받을 수 있고, 열 때 비밀번호 qswujuq5 를 입력하면 됩니다.

실제로 겪은 함정은 드래그를 받는 항목 다섯 개 사이에서도 터치 대응이 갈린다는 점입니다. 01·02·04·07 은 드래그 표면에 touch-action: none 을 걸어 두어 가로 드래그가 페이지 세로 스크롤과 부딪히지 않게 막아 두었는데, 03 의 스타일에는 이 규칙이 없습니다. 실제 터치 기기에 03 을 그대로 옮겨 붙이면 좌우로 카드를 밀려는 손가락 움직임이 페이지를 세로로 스크롤시켜 버리는 충돌이 날 수 있어, 붙여 쓸 때는 .viewport 에 touch-action: none(또는 pan-y)을 함께 추가해야 합니다.

reduce 상태에서 각자 무엇을 멈추는가

01~04·06~09 는 reduce 가 켜져 있어도 실제 드래그·클릭·키보드 조작에서 나오는 계산(관성 감쇠·스프링·원근 보간·스냅·이징)을 그대로 돌립니다 — 여기서 멈추는 건 저절로 도는 시연 루프뿐이고, 조작에 대한 반응 자체는 끄지 않습니다. 05 는 다릅니다. playing 값이 reduce 로 시작해 실사용 자동재생 자체를 완전히 멈추고, 시연용 hover 시뮬레이션도 reduce 에서는 건너뜁니다. 저절로 넘어가는 움직임은 장식이지만, 나머지 여덟 항목의 반응형 계산은 사람이 직접 조작할 때 필요한 기능이기 때문입니다.

키보드·스크린리더로 확인할 것

아홉 항목 모두 캐러셀 영역에 role="region"·aria-roledescription="carousel" 을 붙이고, 이전·다음 버튼에 각각 aria-label="이전 카드"·aria-label="다음 카드" 를 달아 두었습니다. 카드가 바뀔 때마다 aria-live="polite" 인 읽기 전용 영역이 몇 번째 카드인지 문구로 갱신해 스크린리더가 읽어 줍니다. 05 의 재생·정지 버튼은 aria-pressed·aria-label(자동재생 멈춤/자동재생 다시 시작)을 눌린 상태에 맞춰 바꿔, 지금 자동재생 중인지를 버튼 자체에서 알립니다. 포인터 입력 규격은 MDN Pointer events 문서를, prefers-reduced-motion 은 MDN prefers-reduced-motion 문서를, 07 의 스냅 정렬과 가까운 CSS 표준 스냅은 MDN scroll-snap-type 문서를 참고하면 됩니다.

변형 세 가지와 접근성

변형 값 체감
관성 감쇠 계수 낮추기 01 MOMENTUM_FRICTION 0.10 → 0.05 관성 감쇠 계수를 0.10 에서 0.05 로 낮추면 손을 놓은 뒤 카드가 더 오래, 더 멀리까지 미끄러진다.
자동재생 전환 간격 좁히기 05 AUTOPLAY_MS 1350 → 900 AUTOPLAY_MS 를 1350 에서 900 으로 좁히면 카드가 더 자주 넘어가 전체 흐름이 빨라진 것처럼 느껴진다.
스냅 정렬 이동 지속 시간 늘리기 07 SNAP_EASE 0.24 → 0.12 SNAP_EASE 를 0.24 에서 0.12 로 낮추면 손을 놓은 뒤 카드가 제자리를 찾아가는 움직임이 더 느긋하게(더 오래) 보인다.

세 변형 모두 상수 하나만 바뀔 뿐, 이전·다음 버튼과 키보드 리스너 구성이나 role·aria-label·aria-live 흐름은 손대지 않습니다. 어떤 값을 골라도 읽어 주는 문구와 접근성 지원 범위는 그대로 유지됩니다.

캐러셀 자동 슬라이드 예제는 어디서 보나?

이 편 위쪽 3×3 격자가 바로 그 예제이고, 격자 위에서 캐러셀 애니메이션 아홉 개를 한 번에 비교해 볼 수 있습니다. 각 항목 아래 코드 펜스에서 계산부를 그대로 확인할 수 있고, 회전 카드 축을 더 보려면 043·082 를, 좌우 비교 슬라이더를 보려면 212 를 함께 열어보면 됩니다.

FAQ

캐러셀 슬라이드 예제 코드를 그대로 다른 프로젝트에 써도 되나요?

네, 됩니다. zip 안 코드는 라이브러리·CDN 없이 HTML·CSS·JS 만으로 동작하는 vanilla 코드라 스크립트 태그와 스타일만 옮겨 붙이면 그대로 돕니다. 다만 03 처럼 touch-action 을 따로 걸어야 하는 항목은 실제 서비스에 붙일 때 그 규칙을 같이 옮겨야 합니다.

reduced-motion을 켠 화면에서도 아홉 가지 전환이 전부 되나요?

네, 됩니다. reduce 가 막는 건 저절로 도는 시연 루프와(05 의 경우) 자동재생 자체뿐이고, 나머지 조작 — 드래그·클릭·키보드로 카드를 넘기는 동작 — 은 01~04·06~09 어디서나 reduce 값과 무관하게 그대로 계산됩니다.

터치 스와이프와 마우스 드래그를 둘 다 지원하나요?

항목마다 다릅니다. 01·02·03·04·07 은 pointerdown·pointermove·pointerup·pointercancel 로 터치와 마우스 드래그를 같은 코드로 받아 관성·고무줄·원근·무한 루프·스냅 판정을 계산합니다. 05·06·08·09 는 드래그 대신 클릭·키보드(그리고 08 은 휠)로만 카드를 넘기는 구조입니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.