CSS 슬라이더로 만드는 캐러셀 9가지 — 복붙 코드
CSS 슬라이더는 사진이나 카드 여러 장이 화면 안에서 옆으로 또는 위아래로 순서대로 넘어가며 보이게 하는 화면 부품입니다. 스크롤부터 라디오 트릭까지, 자바스크립트 없이 CSS만으로 만드는 아홉 가지 방식을 그대로 복사해서 쓸 수 있게 모았습니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 다음 슬라이드 살짝 보임
- 02 라디오+화살표 슬라이더
- 03 :target 도트 내비
- 04 썸네일 연동 스냅
- 05 세로 스냅
- 06 커버플로우 스냅
- 07 카드 스택 스와이프
- 08 켄번즈 줌 자동재생 히어로
- 09 스크롤 연동 진행 바
아홉 개는 인기순이 아니라 캐러셀을 실제로 만들 때 손이 가는 순서입니다. 손가락으로 밀어 넘기는 가장 기본형(01)에서 화살표·점·썸네일로 조작 방법을 바꿔보는 것(02~04)으로 넘어가고, 그다음은 방향과 모양을 바꿔보는 것(05~06), 마지막은 아무것도 안 눌러도 저절로 움직이거나 지금 어디쯤인지 알려주는 것(07~09) 순서입니다.
01다음 슬라이드 살짝 보임
카드 사이 여백을 넉넉히 남겨 옆 칸의 슬라이드가 화면 끝에 살짝 걸쳐 보이게 하고, scroll-padding-left로 첫 칸이 화면 왼쪽 끝에 딱 맞춰 멈추게 합니다. 쇼핑몰 상품 목록처럼 "더 있다"는 걸 보여주고 싶을 때 씁니다.
.pn__viewport {
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-left: 16px;
}
.pn__track { display: flex; width: max-content; }
.pn__slide {
flex: 0 0 200px;
scroll-snap-align: start;
margin-right: 12px;
}
02라디오+화살표 슬라이더
숨긴 라디오 버튼 세 개와 화살표 라벨만으로 슬라이드를 한 칸씩 넘기는, 스크롤 없이도 되는 가장 오래된 CSS 캐러셀 방식입니다. 배너나 공지 카드처럼 손으로만 넘기게 하고 싶을 때 씁니다.
.ra__track {
display: flex;
width: 300%;
transition: transform .3s;
}
#ra-1:checked ~ .ra__viewport .ra__track { transform: translateX(0%); }
#ra-2:checked ~ .ra__viewport .ra__track { transform: translateX(-33.3333%); }
#ra-3:checked ~ .ra__viewport .ra__track { transform: translateX(-66.6667%); }
03:target 도트 내비
점을 누르면 실제 링크처럼 주소창의 #이 바뀌고, 브라우저가 그 슬라이드를 스크롤 상자 안에서 화면에 맞춰 이동시킵니다. 지금 가리키는 슬라이드는 :target 선택자로 판별해 그 슬라이드에 해당하는 점만 칠합니다. 뒤로가기를 누르면 이전 슬라이드로 돌아가야 하는 갤러리에 알맞습니다.
.dt__viewport { overflow-x: auto; scroll-snap-type: x mandatory; }
.dt__slide { scroll-snap-align: start; }
.dt:has(#dt-s1:target) .dt__dot--1,
.dt:has(#dt-s2:target) .dt__dot--2,
.dt:has(#dt-s3:target) .dt__dot--3 {
background: var(--color);
transform: scale(1.4);
}
04썸네일 연동 스냅
큰 사진 아래 진짜 썸네일 이미지 줄이 따로 있고, 각 썸네일은 대응하는 슬라이드로 가는 앵커라 :target으로 지금 보는 사진의 썸네일에만 테두리가 켜집니다. 제품 상세 이미지처럼 작은 미리보기로 바로 건너뛰게 하고 싶을 때 씁니다.
.tm__thumb {
border: 3px solid transparent;
transition: border-color .3s;
}
.tm:has(#tm-s1:target) .tm__thumb--1,
.tm:has(#tm-s2:target) .tm__thumb--2,
.tm:has(#tm-s3:target) .tm__thumb--3 {
border-color: #17141a;
}
05세로 스냅
가로가 아니라 세로로 스크롤하며 화면 하나에 슬라이드 하나씩 딱 맞춰 멈추는, 휴대폰 스토리 화면과 같은 방향의 캐러셀입니다. 세로형 배너나 모바일 풀스크린 소개 화면에 씁니다.
.vs__viewport {
height: 100%;
overflow-y: auto;
scroll-snap-type: y mandatory;
}
.vs__track { display: flex; flex-direction: column; }
.vs__slide { flex: 0 0 150px; scroll-snap-align: start; }
06커버플로우 스냅
라디오 없이 실제 가로 스크롤과 scroll-snap-align: center만으로, 가운데 카드는 정면을 보고 양옆 카드는 자기 자리에서부터 이미 비스듬히 기울어 있습니다. 포트폴리오 작품이나 앨범 커버를 넘기는 화면에 어울립니다.
.cf__viewport {
overflow-x: auto;
scroll-snap-type: x mandatory;
perspective: 640px;
}
.cf__slide { scroll-snap-align: center; }
.cf__slide--1, .cf__slide--5 { transform: rotateY(38deg) scale(.72); }
.cf__slide--2, .cf__slide--4 { transform: rotateY(24deg) scale(.86); }
.cf__slide--3 { transform: rotateY(0deg) scale(1); }
07카드 스택 스와이프
카드 여러 장을 같은 자리에 겹쳐 쌓아두고, 맨 위 카드가 옆으로 미끄러지며 빠지면 다음 카드가 그 자리를 채우는 방식이라 스크롤 상자 자체가 없습니다. 추천 카드를 한 장씩 훑어보는 화면에 씁니다.
.cs__card {
grid-area: 1 / 1;
animation: cycle 6s ease-in-out infinite;
}
@keyframes cycle {
0%, 4% { transform: translate(0, 0); opacity: 1; z-index: 3; }
29% { transform: translate(42%, 6%) rotate(12deg); opacity: 0; }
33%, 62% { transform: translate(-6%, 6%) rotate(-4deg); opacity: .85; }
66%, 96% { transform: translate(6%, 4%) rotate(3deg); opacity: .92; }
100% { transform: translate(0, 0); opacity: 1; z-index: 3; }
}
08켄번즈 줌 자동재생 히어로
사진이 저절로 넘어가는 동안 지금 보이는 사진만 천천히 확대돼, 정지 화면인데도 계속 살아 움직이는 느낌을 줍니다. 메인 히어로 배너나 브랜드 소개 풀스크린 슬라이드에 씁니다.
.kb__card { grid-area: 1 / 1; animation: fade1 6s linear infinite; }
.kb__img { animation: zoom1 6s ease-out infinite; }
@keyframes fade1 { 0%, 4% { opacity: 1; } 29%, 100% { opacity: 0; } }
@keyframes zoom1 {
0% { transform: scale(1); }
29% { transform: scale(1.16); }
29.1%, 100% { transform: scale(1); }
}
09스크롤 연동 진행 바
라디오나 자바스크립트로 계산하지 않고, animation-timeline이 실제 가로 스크롤 위치를 그대로 읽어 진행 바를 채웁니다. 슬라이드가 몇 장인지 한눈에 보여주고 싶은 스토리 배너에 씁니다.
.sp { timeline-scope: --sp-progress; }
.sp__viewport { scroll-timeline: --sp-progress inline; }
.sp__fill {
animation-name: fill;
animation-timeline: --sp-progress;
animation-fill-mode: both;
}
@keyframes fill { from { width: 0%; } to { width: 100%; } }
어디서 깨지는가 — 함정 하나
09번(스크롤 연동 진행 바)에서 가장 크게 걸린 함정은 진행 바와 스크롤 상자가 형제 요소라는 점이었습니다. scroll-timeline으로 이름 붙인 타임라인은 그 이름을 선언한 요소의 자손에게만 기본으로 보입니다. 진행 바(.sp__fill)는 스크롤 상자(.sp__viewport) 밖에, 그 위에 따로 있는 막대라서 처음엔 animation-timeline: --sp-progress를 걸어도 진행률이 전혀 움직이지 않았습니다. 공통 조상에 timeline-scope: --sp-progress를 선언해 그 이름을 형제 쪽까지 열어 주고 나서야 막대가 실제 스크롤을 따라 채워졌습니다. animation-timeline과 timeline-scope는 설치된 크롬에서 직접 렌더해 확인한 기능이라, 아직 못 따라온 브라우저에서는 막대가 그냥 비어 있을 뿐 슬라이드 넘기기 자체는 멀쩡히 돌아갑니다. 아홉 개 폴더를 전부 이렇게 다시 확인하고서야 압축했고, b2earrkf을 문서 안내대로 넣으면 고친 결과가 그대로 눈앞에서 열립니다.
접근성
아홉 개 모두 prefers-reduced-motion: reduce에서 자동으로 움직이는 부분(07·08의 자동 순환, 01·03·04·05·06·09의 미리보기 루프)이 꺼지고 지금 상태로 고정됩니다. 스크롤 스냅을 쓰는 01·03·04·05·06·09는 Tab과 화살표 키만으로 다음 칸으로 넘어가고, 02의 라디오 슬라이더는 네이티브 폼 컨트롤이라 화살표 키로 바로 선택이 바뀝니다. 03·04의 점·썸네일에는 aria-label="슬라이드 N"을 달아 스크린 리더가 몇 번째인지 구분할 수 있게 했습니다.
FAQ
라디오 슬라이더와 스크롤 스냅 중 뭘 써야 하나요?
실제로 드래그해서 넘기는 느낌이 필요하면 스크롤 스냅(01·03~06·09)을, 화살표나 점만으로 딱 떨어지게 넘기고 싶으면 라디오 방식(02)을 씁니다. 스크롤 스냅은 터치 기기에서 손가락으로 바로 밀 수 있다는 게 강점이고, 라디오 방식은 트랙 위치를 라디오 하나로 정확히 고정할 수 있다는 게 강점입니다.
animation-timeline은 모든 브라우저에서 되나요?
설치된 크롬으로 직접 렌더해 값을 재 본 결과라 크로미움 밖은 장담하지 못합니다. 다만 09번의 진행 바만 못 움직일 뿐 스크롤 스냅 자체는 그대로라, 슬라이드를 넘기는 데는 지장이 없습니다.
React 버전은 원리가 다른가요?
01·05·06·07·08·09는 CSS 모듈에 같은 스크롤 스냅·키프레임·animation-timeline을 그대로 옮겨서 원리가 동일합니다. 02(라디오)는 useState 하나로 바꿨고, 03·04(:target)는 여러 인스턴스를 한 페이지에 놓아도 아이디가 겹치지 않도록 scrollIntoView로 바꿨습니다. duration·easing·color는 모두 props로 받습니다.
CSS만으로 만드는 다른 애니메이션은 CSS 애니메이션 카테고리에서 더 찾을 수 있고, 사이트 운영 방식은 소개 페이지에 적어 뒀습니다. scroll-snap-type의 정확한 동작은 MDN scroll-snap-type 문서에서, animation-timeline은 MDN animation-timeline 문서에서 확인할 수 있습니다.