GODRICH

이미지 슬라이드 만들기 9종 — CSS 캐러셀 그대로 복사

이미지 슬라이드 만들기는 쇼핑몰이나 포트폴리오 사이트에서 사진 몇 장이 옆으로 넘어가며 순서대로 보이는 화면을 만드는 일입니다. AOS·GSAP 없이, JS 0줄로 CSS만 붙여 넣어도 아홉 가지 방식으로 만들 수 있습니다.

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

아홉 개는 인기 순이 아니라 캐러셀이 화면에서 맡는 역할 순서로 놓았습니다. 손으로 미는 가장 기본형(01)에서 점·화살표·썸네일로 조작 방법을 보여주는 것(02~04)으로 넘어가고, 그다음은 손대지 않아도 저절로 도는 것(05~06), 마지막은 지금 뭘 보고 있는지 강조하는 장식형(07~09) 순서입니다.

01기본 scroll-snap

네모난 사진 카드 네 장이 가로로 나란히 있고, 손가락이나 마우스 휠로 밀면 카드가 화면에 딱 맞춰 멈춰 섭니다. overflow-x:autoscroll-snap-type 두 속성만 붙이면 끝나서 자바스크립트가 한 줄도 없습니다.

scroll-snap-typescroll-snap-alignJS 0줄
.cs__viewport {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}
.cs__track { display: flex; width: 400%; }
.cs__slide {
  flex: 0 0 25%;
  scroll-snap-align: start;
}

02::scroll-marker 도트

슬라이드 아래 작은 점들이 있고, 지금 보고 있는 사진의 점만 색이 채워지고 살짝 커집니다. 점은 자바스크립트로 그린 게 아니라 브라우저가 ::scroll-marker로 슬라이드마다 하나씩 자동으로 만들어 줍니다.

::scroll-markerscroll-marker-group:target-current
.viewport { scroll-marker-group: after; }
.slide::scroll-marker {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
}
.slide::scroll-marker:target-current {
  background: $color;
  transform: scale(1.35);
}

03::scroll-button 화살표

슬라이드 양옆에 이전·다음 화살표 버튼이 있고, 누르면 사진이 한 칸씩 부드럽게 넘어갑니다. 화살표 버튼도 브라우저가 ::scroll-button()으로 자동으로 만들고, 맨 끝에서는 흐려지도록 스타일을 입힙니다.

::scroll-button(*):disabledscroll-snap-type
.viewport::scroll-button(left)  { content: "‹"; }
.viewport::scroll-button(right) { content: "›"; }
.viewport::scroll-button(left):disabled,
.viewport::scroll-button(right):disabled {
  opacity: 0;
  pointer-events: none;
}

04썸네일 마커

큰 사진 아래 작은 정사각 썸네일들이 줄지어 있고, 지금 보는 큰 사진과 같은 색의 썸네일에만 테두리가 생깁니다. 썸네일도 ::scroll-marker 하나로 만들고, 색은 원본 슬라이드에 적어 둔 CSS 변수를 그대로 물려받습니다.

::scroll-markerCSS 변수 상속:target-current
.slide { background: var(--thumb); }
.slide::scroll-marker {
  content: "";
  background: var(--thumb);
  border: 2px solid transparent;
}
.slide::scroll-marker:target-current {
  border-color: $color;
}

05CSS만으로 자동재생

아무것도 누르지 않아도 사진이 몇 초마다 저절로 다음 장으로 넘어갑니다. 마우스를 올리면 잠깐 멈춰서 원하는 사진을 편하게 볼 수 있습니다.

@keyframesanimation-play-stateJS 0줄
.ap__track {
  display: flex;
  width: 400%;
  animation: ap-cycle $duration $easing infinite;
}
.ap:hover .ap__track { animation-play-state: paused; }
@keyframes ap-cycle {
  0%, 17%  { transform: translateX(0%); }
  25%, 42% { transform: translateX(-25%); }
  50%, 67% { transform: translateX(-50%); }
  75%, 92% { transform: translateX(-75%); }
  100%     { transform: translateX(0%); }
}

06후기 슬라이더

고객 후기 카드가 한 장씩 페이드로 바뀌며 저절로 순환합니다. 후기 글자 길이가 서로 달라도 카드들을 같은 자리에 겹쳐 둬서 높이가 흔들리지 않습니다.

opacitygrid 겹침@keyframes
.ts__card { grid-area: 1 / 1; opacity: 0; }
.ts__card--1 { animation: ts-1 $duration $easing infinite; }
.ts__card--2 { animation: ts-2 $duration $easing infinite; }
.ts__card--3 { animation: ts-3 $duration $easing infinite; }
@keyframes ts-1 { 0%, 4%   { opacity: 1; } 29%, 100% { opacity: 0; } }
@keyframes ts-2 { 0%, 29%  { opacity: 0; } 33%, 62%  { opacity: 1; } 66%, 100% { opacity: 0; } }
@keyframes ts-3 { 0%, 62%  { opacity: 0; } 66%, 96%  { opacity: 1; } 100%     { opacity: 0; } }

07활성 슬라이드 확대

가운데 있는 사진만 조금 크게 확대되고 양옆 사진은 작아지고 흐려져서, 지금 보고 있는 게 뭔지 한눈에 들어옵니다. 원하는 사진을 클릭하면 그 사진이 가운데로 오며 다시 커집니다.

:checkedtransform: scaleopacity
.slide {
  transform: scale(.82);
  opacity: .5;
  transition: transform .3s, opacity .3s;
}
#as-2:checked ~ .as__track .as__slide--2 {
  transform: scale(1.08);
  opacity: 1;
}

08진행 마커 연동

슬라이드 위쪽에 얇은 진행 바가 있고, 사진이 넘어갈 때마다 그만큼 채워집니다. 진행 바를 직접 눌러도 누른 자리의 사진으로 바로 넘어갑니다.

:checkedwidth 전환형제결합자
.pm__fill {
  width: calc(100% / 3);
  transition: width .3s;
}
#pm-2:checked ~ .pm__bar .pm__fill { width: calc(200% / 3); }
#pm-3:checked ~ .pm__bar .pm__fill { width: 100%; }

093D 커버플로우

가운데 사진은 정면으로 크게 서 있고 양옆 사진들은 비스듬히 기울어진 채 작게 물러나 있어서, 앨범 커버를 손으로 넘기는 것처럼 보입니다. 원하는 사진을 클릭하면 그 사진이 정면 가운데로 옵니다.

perspectiverotateY + translateZ:checked
.cf__stage { perspective: 720px; }
.cf__slide { transform-style: preserve-3d; }
#cf-2:checked ~ .cf__stage .cf__slide--2 {
  transform: translateX(-50%) rotateY(0deg) scale(1);
}
#cf-2:checked ~ .cf__stage .cf__slide--3 {
  transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(.74);
}

어디서 깨지는가 — 함정 하나

아홉 개 중 가장 크게 걸린 함정은 자동 재생 미리보기를 만들 때였습니다. 스크롤 상자(overflow-x:auto가 걸린 요소) 자체에 translateX를 걸었더니 카드 전체가 통째로 화면 밖으로 사라졌습니다. translateX(-100%)의 100%는 항상 그 요소 자기 자신의 너비 기준이라, 스크롤 상자 안에 있는 카드 네 장짜리 콘텐츠 폭이 아니라 상자 자신의 폭(카드 한 장 크기)만큼만 밀린 것입니다. getBoundingClientRect로 직접 재 보니 원했던 이동량은 카드 네 장을 합친 폭(1600px 안팎)이었는데 실제로는 딱 카드 한 장 폭(400px)만 움직이고 있었고, 그 차이만큼 카드가 뷰포트 밖으로 완전히 밀려나 화면엔 무대 배경만 남았습니다. 그래서 스크롤 상자는 그대로 두고 그 안의 콘텐츠 줄(트랙)에만 transform을 걸어야 실제 드래그·스냅은 그대로 살아있으면서 자동 재생도 됩니다. 이 압축 파일의 9jk4bv6c은 이렇게 바로잡은 아홉 개 파일을 엽니다.

::scroll-button():disabled 상태(맨 끝에서 화살표가 흐려지는 것)는 테스트해 보니 크로미움 계열에서도 완전히 깔끔하지는 않아서, 화살표가 옅게 남더라도 실제로는 더 눌리지 않도록 스크롤 경계에서 동작이 멈추게 해 뒀습니다.

접근성

아홉 개 모두 prefers-reduced-motion: reduce에서 자동 재생 애니메이션이 꺼지고 지금 보고 있는 슬라이드 상태로 고정됩니다. 라디오로 만든 07·08·09는 네이티브 폼 컨트롤이라 Tab과 화살표 키로 바로 움직이고, ::scroll-marker·::scroll-button을 쓴 02·03·04는 포커스가 가면 화살표 키만으로 슬라이드가 넘어갑니다. 07·09의 각 카드에는 aria-label="슬라이드 N"을 달아 스크린 리더가 카드끼리 구분할 수 있게 했습니다.

포트폴리오·랜딩페이지에 쓸 다른 CSS 애니메이션은 CSS 애니메이션 카테고리에서 더 찾을 수 있고, 사이트 운영 방식은 소개 페이지에 적어 뒀습니다. scroll-snap-type의 정확한 동작은 MDN scroll-snap-type 문서에서 확인할 수 있습니다.

FAQ

::scroll-marker·::scroll-button은 모든 브라우저에서 되나요?

이 글을 쓰는 시점 기준으로 크로미움 계열 브라우저에서 동작을 직접 확인했습니다. 아직 지원하지 않는 브라우저에서는 점·화살표만 안 보일 뿐 scroll-snap-type 자체는 정상 동작해서, 손가락이나 휠로 미는 기본형(01)으로 자연스럽게 되돌아갑니다.

자동 재생 중에 사용자가 드래그하면 어떻게 되나요?

슬라이드를 처음 건드리는 순간(pointerdown) 자동 재생 클래스가 바로 빠지고, 그 뒤로는 사용자의 스크롤·클릭만 반영됩니다. 자동 재생과 실제 조작이 동시에 싸우는 일은 없습니다.

React 버전은 원리가 다른가요?

01~04는 CSS 모듈에 같은 scroll-snap-type·::scroll-marker·::scroll-button()을 그대로 옮겨서 원리가 동일합니다. 07~09처럼 원래 라디오 입력으로 고르던 항목만 useState 하나로 바꾸고, duration·easing·color는 props로 받습니다.

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

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