Carousel
한 장씩 보이는 슬라이드 띠. 손가락으로 밀 수 있고, 키보드로 넘길 수 있고, RTL에서도 맞습니다 — 밑에 깔린 장치가 브라우저 자신의 스크롤이기 때문입니다.
import { Carousel } from 'neba';
<Carousel label="제품 소개">
<img src="/one.jpg" alt="" />
<img src="/two.jpg" alt="" />
</Carousel>;슬라이드는 서브컴포넌트가 아닙니다. 최상위 자식 하나가 슬라이드 하나가 되므로 <Carousel><img /><img /></Carousel>이 API 전부입니다. 스냅 지점, 폭, 스크린 리더가 필요로 하는 role="group" / aria-roledescription="slide" 짝은 감싸는 쪽이 붙입니다 — 사진 한 장에 그것들을 잊지 않고 붙여야 할 사람은 아무도 없어야 하니까요.
화살표는 프레임 위에 그려집니다. 사진에는 맞고 문단에는 맞지 않는 방식이므로, 가장자리 가까이에 글자가 있는 슬라이드는 화살표를 피할 만큼 안쪽으로 여백을 두어야 합니다 — 대략 컨트롤 높이에 안쪽 여백을 더한 만큼, size="md"에서 3.5rem 정도입니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'outline' | 'text' | 'outline' | 프레임의 무게. 컨테이너의 방식대로 시트에 색을 들이지 않습니다 — 캐러셀은 남의 사진을 담습니다. 사진에 이미 테두리가 있으면 text |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 프레임의 모서리, 화살표의 크기와 안쪽 여백, 점의 크기 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 0은 그림자 없음 |
| value | number | — | 보이는 슬라이드. 0부터 |
| defaultValue | number | 0 | 처음 보이는 슬라이드 |
| onValueChange | (index: number) => void | — | 슬라이드가 바뀔 때. 손가락으로 밀어서 바뀐 경우에도 불립니다 |
| loop | boolean | true | 끝에서 처음으로 돌아갈지. 끄면 양 끝에서 화살표가 죽습니다 — 시작과 끝이 있는 묶음에는 그쪽이 정직합니다 |
| autoPlay | boolean | false | 스스로 넘어갑니다. 기본이 꺼짐인 이유는 읽는 중에 움직이는 캐러셀이 웹에서 가장 많은 불평을 듣는 패턴이기 때문입니다. 켜도 hover·포커스·백그라운드 탭에서 멈추고, 모션을 줄여 달라고 한 독자에게는 아예 시작하지 않습니다 |
| interval | number | 5000 | 슬라이드 한 장을 붙잡는 시간(ms) |
| arrows | boolean | true | 이전/다음 버튼 |
| indicators | boolean | true | 프레임 아래 위치 점들 |
| label | string | 'Carousel' | 캐러셀의 접근성 이름. 선택이 아니라 기본값입니다 — 이름 없는 region은 건너뛸 수도 없습니다 |
| previousLabel | string | 'Previous slide' | 이전 버튼의 이름 |
| nextLabel | string | 'Next slide' | 다음 버튼의 이름 |
| slideLabel | (index: number, count: number) => string | — | 슬라이드 하나를 스크린 리더에게 어떻게 부를지, 그리고 그 점의 라벨. 기본값은 `Slide ${index} of ${count}` |
| children | ReactNode | — | 슬라이드들. 최상위 자식 하나가 슬라이드 하나가 됩니다 — 스냅 지점, 폭, role을 감싸는 쪽이 붙이므로 사진 한 장에 그것들을 붙일 일이 없습니다 |
<div>의 네이티브 속성은 그대로 전달됩니다.
예시
순환, 장식, 자동 재생
장식은 선택이고 양 끝은 결정입니다. loop를 끄면 화살표가 끝에서 감기는 대신 죽는데, 처음과 끝이 있는 묶음에는 그쪽이 정직합니다.
Controlled
value / onValueChange는 늘 쓰던 그 짝이라, 띠를 페이지의 다른 것이 몰 수 있습니다 — 마법사의 단계 버튼, 라우터, 단축키. 누군가 손가락으로 밀어서 슬라이드가 바뀐 경우에도 onValueChange는 불립니다.
미끄러지지 않고 스크롤하는 이유
이 띠는 CSS 스크롤 스냅이 걸린 스크롤 컨테이너이고, 이 컴포넌트의 좋은 점은 전부 그 결정 하나에서 따라 나옵니다.
- 밀기가 됩니다. 휴대폰에서도 트랙패드에서도 — 제스처 핸들러가 흉내 낸 것이 아니라 브라우저 자신의 스크롤이기 때문입니다.
- RTL에서 반대로 흐릅니다. 따로 말해 주지 않아도 그렇습니다. 스크롤에는 방향이 있고
translate에는 없기 때문입니다. - 아무것도 변형되지 않습니다. 표면을 움직이지 말라는 이 집의 규칙이 여기서는 공짜로 지켜집니다.
translate로 트랙을 밀었다면 예외를 주장해야 했을 자리입니다. - 모션은
scroll-behavior: smooth입니다. 그래서 모션을 줄여 달라고 한 독자는 그 사람을 기억하려고 따로 쓴 두 번째 경로가 아니라 같은 경로에서 즉각적인 전환을 받습니다.
자동 재생, 그리고 꺼져 있는 이유
읽는 중에 움직이는 캐러셀은 웹에서 가장 많은 불평을 듣는 패턴입니다. autoPlay는 기본이 꺼짐이고, 켜더라도 hover, 안쪽 어디든의 포커스, 백그라운드 탭에서 멈추며, 모션을 줄여 달라고 한 독자에게는 아예 시작하지 않습니다. 현재 슬라이드를 알리는 라이브 영역도 자동 재생 중에는 침묵합니다 — 5초마다 새 이름을 말하는 라이브 영역이야말로 그것이 있는 페이지에서 스크린 리더를 못 쓰게 만드는 것이기 때문입니다.
슬라이드 묶음이 전부 읽혀야 할 만큼 중요하다면, 그것은 캐러셀이 아니어야 할 만큼 중요합니다. Tabs나 그냥 세로로 쌓는 편이 낫습니다.
제공하지 않는 것
- 한 화면에 여러 장. 살짝 보이는 캐러셀은 브레이크포인트별 폭 계산이 필요하고, 그것의 정직한 형태는
overflow-x-auto를 얹은 Grid입니다. - 세로. 세로 캐러셀은 스크롤되는 목록이고, 페이지가 이미 그것입니다.
- 페이드. 장치가 스크롤인데 페이드까지 하면 장치가 둘이 됩니다.
다른 라이브러리에서 옮겨올 때
| 다른 곳 | Neba |
|---|---|
<Carousel.Item> | 필요 없습니다. 최상위 자식 하나가 슬라이드 하나입니다 |
activeIndex / onSelect | value / onValueChange — Neba의 모든 컴포넌트가 쓰는 짝 |
멈추려고 interval={null} | autoPlay={false}, 이미 기본값입니다 |
wrap | loop |
indicators / controls | indicators / arrows |
slidesPerView | 없습니다 — 위 참고 |