Carousel
이미지를 슬라이드 형태로 표시하고, 클릭 시 외부 콜백을 호출하는 캐러셀 컴포넌트
개요
이미지 목록을 좌우 슬라이드로 표시합니다. 화살표와 인디케이터로 탐색하며, 자동 재생(autoplay)을 지원합니다. onImageClick을 넘기면 이미지 클릭을 콜백으로 받습니다.
주요 특징
- ✅ 단일/다중 슬라이드: 이미지가 2장 이상이면 화살표와 인디케이터를 표시
- ✅ 자동 재생: autoplay/delay로 자동 슬라이드
- ✅ 화살표 모드: always, hover, never 세 가지 표시 방식
- ✅ 클릭 콜백: onImageClick을 넘기면 슬라이드가 버튼이 되고 클릭을 콜백으로 위임
- ✅ embla 옵션: opts로 loop·드래그 등 embla-carousel 옵션을 그대로 전달
- ✅ 문구: labels로 이미지 없음 alt·인디케이터 이름을 바꿈
- ✅ 슬라이드 이벤트: onSlideChange로 현재/이전 인덱스와 현재 이미지 전달
기본 사용
images에 { id, src } 목록을 전달합니다. 1장이면 화살표/인디케이터가 표시되지 않습니다.
Preview
슬라이드를 직접 넣기 (children)
Carousel.Item을 children으로 넣으면 이미지가 아닌 내용도 슬라이드로 쓸 수 있습니다. children을 주면 images보다 이기고, onImageClick은 적용되지 않으며 onSlideChange·ref의 currentImage는 undefined입니다. 화살표·인디케이터·자동 재생·높이·labels·opts는 그대로 적용됩니다.
Carousel.Item의 className은 슬라이드 콘텐츠 박스에 붙습니다. 슬라이드 사이 간격은 바깥 요소가 가지므로 배경을 줘도 간격까지 번지지 않습니다.
<Carousel height={240}>
<Carousel.Item>
<div className="flex h-full items-center justify-center">안내 1</div>
</Carousel.Item>
<Carousel.Item>
<div className="flex h-full items-center justify-center">안내 2</div>
</Carousel.Item>
</Carousel>자동 재생 (Autoplay)
autoplay를 설정하면 지정된 delay(ms, 기본 4000) 간격으로 자동으로 슬라이드가 전환됩니다. 마우스를 올리거나 포커스가 캐러셀 안으로 들어오면 멈추고, 벗어나면 다시 재생됩니다. 운영체제의 움직임 줄이기(prefers-reduced-motion: reduce)가 켜져 있으면 자동 재생하지 않습니다.
<Carousel images={images} autoplay delay={3000} height={240} />화살표 표시 방식 (arrows)
| 값 | 동작 |
|---|---|
always | 항상 표시 (기본) |
hover | 마우스를 올렸거나 키보드 포커스가 들어왔을 때만 표시 |
never | 화살표 숨김 (인디케이터만으로 탐색) |
<Carousel images={images} arrows="hover" />인디케이터 (indicators)
슬라이드 수만큼 점을 그려 현재 위치를 보이고, 점을 누르면 해당 슬라이드로 이동합니다. 이미지가 2장 이상일 때만 그리고 기본은 true입니다. 점의 이름은 labels.slide(n)(기본 “Slide n”)입니다.
<Carousel images={images} indicators={false} />이미지 클릭 (onImageClick)
onImageClick을 넘기면 모든 슬라이드가 버튼으로 그려지고 클릭 시 콜백이 호출됩니다. 넘기지 않으면 클릭 동작이 없습니다. 콜백의 image는 images의 원본 항목이라, 항목에 더 실은 필드(예: link)를 그대로 되받습니다.
<Carousel
images={[
{ id: "1", src: "...", link: "/page-a" },
{ id: "2", src: "...", link: "/page-b" },
]}
onImageClick={({ image, index }) => {
// image — 클릭된 항목 (image.link 포함)
// index — 슬라이드 인덱스
}}
/>embla 옵션 (opts)
opts는 embla-carousel 옵션 객체를 그대로 넘깁니다. loop(끝에서 처음으로 이음), watchDrag(끌어서 넘기기), startIndex, align, dragFree, slidesToScroll 등을 쓸 수 있습니다. 기본값은 { loop: true, watchDrag: 슬라이드 2장 이상 }이고 넘긴 값이 이깁니다.
// 마지막 슬라이드에서 멈춘다
<Carousel images={images} opts={{ loop: false }} />
// 끌어서 넘기기를 끈다
<Carousel images={images} opts={{ watchDrag: false }} />문구 (labels)
labels는 지정한 항목만 기본값(영어)을 덮습니다. 한국어 화면이면 네 항목을 모두 지정합니다.
| 항목 | 기본값 | 쓰는 곳 |
|---|---|---|
carousel | ”Image carousel” | 캐러셀 영역(region)의 이름. aria-label을 직접 주면 그것이 우선 |
noImage | ”No image” | 대체 이미지의 alt, 대체 아이콘의 화면 낭독용 문구 |
slide(n) | ”Slide n” | 인디케이터 점의 이름, alt 없는 클릭 슬라이드의 이름 |
slideOf(n, total) | ”n / total” | 슬라이드(group)의 이름 |
<Carousel images={images} labels={{ slide: (n) => `Slide ${n}`, noImage: "No image" }} />이미지 맞춤
images 경로의 이미지는 foundation Image로 그리고 object-cover로 고정해 맞춥니다. 맞춤을 바꾸는 prop은 없습니다. 다르게 맞추려면 children으로 슬라이드를 직접 넣고, Carousel.Item 안에 foundation Image를 두어 className으로 정합니다. Image는 @vortex/ui-cals에서 import합니다.
Carousel.Item의 콘텐츠 박스는 안의 Image를 박스 크기로 채웁니다. 그래서 Image에 size-full과 맞춤 클래스를 주면 슬라이드 높이(height) 안에서 맞춰집니다.
맞춤 값
| 클래스 | CSS | 보이는 모양 |
|---|---|---|
object-cover | object-fit: cover | 비율을 지키며 슬라이드를 가득 채운다. 넘치는 부분은 잘린다. images 경로의 기본 |
object-contain | object-fit: contain | 비율을 지키며 이미지 전체가 보인다. 남는 곳은 여백 |
object-fill | object-fit: fill | 비율을 무시하고 슬라이드에 꽉 맞춘다. 늘어나거나 눌린다 |
object-none | object-fit: none | 원본 크기 그대로. 크면 잘리고 작으면 여백 |
object-scale-down | object-fit: scale-down | none과 contain 중 더 작게 보이는 쪽. 작은 이미지는 원본 크기, 큰 이미지는 전체가 보이게 줄인다 |
보이는 영역은 object-position 클래스로 정합니다: object-center(기본), object-top, object-bottom, object-left, object-right, object-left-top 등. cover로 잘릴 때 어느 쪽을 남길지, contain·none으로 여백이 생길 때 이미지를 어디에 둘지를 정합니다.
AS-IS(el-carousel 슬라이드의 background-image, background-size: auto)는 원본 크기로 가운데에 두었습니다. 가장 가까운 것은 object-none object-center입니다.
예제
import { Carousel, Image } from "@vortex/ui-cals"
// 세로로 긴 이미지: 전체가 보이게 (좌우 여백)
<Carousel height={240}>
<Carousel.Item>
<Image src={portrait} alt="세로 이미지" className="size-full object-contain" />
</Carousel.Item>
</Carousel>
// 세로로 긴 이미지: 가득 채우기 (위아래 잘림)
<Carousel height={240}>
<Carousel.Item>
<Image src={portrait} alt="세로 이미지" className="size-full object-cover" />
</Carousel.Item>
</Carousel>
// 가로로 긴 이미지: 전체가 보이게 (위아래 여백)
<Carousel height={240}>
<Carousel.Item>
<Image src={panorama} alt="파노라마" className="size-full object-contain" />
</Carousel.Item>
</Carousel>
// 채우되 위쪽을 남기기 (인물 사진의 얼굴 등)
<Carousel height={240}>
<Carousel.Item>
<Image src={portrait} alt="인물" className="size-full object-cover object-top" />
</Carousel.Item>
</Carousel>
// 여러 장, 장마다 다른 맞춤
<Carousel
height={240}
onSlideChange={({ currentIndex }) => {
// children 경로라 currentImage 는 undefined. currentIndex 로 구분한다
console.log(currentIndex)
}}
>
<Carousel.Item>
<Image src={portrait} alt="세로" className="size-full object-contain" />
</Carousel.Item>
<Carousel.Item>
<Image src={panorama} alt="가로" className="size-full object-cover" />
</Carousel.Item>
<Carousel.Item>
<Image src={icon} alt="작은 아이콘" className="size-full object-scale-down" />
</Carousel.Item>
</Carousel>한계
Image는Carousel.Item의 직계 자식으로 둡니다. 사이에 다른 래퍼(div등)를 두면 그 래퍼에 높이가 없어Image가 슬라이드 높이를 받지 못할 수 있습니다. 래퍼가 꼭 필요하면 래퍼에h-full을 줍니다.- 콘텐츠 박스가 채우는 대상은 foundation
Image(data-slot="image")입니다. 일반<img>는size-full만으로 채워집니다.
이미지 로드 실패와 빈 목록의 동작은 아래 「이미지 없음」을 봅니다. children 경로의 Image는 foundation Image의 기본 대체 표시를 쓰고, fallbackSrc·labels.noImage는 images 경로에만 적용됩니다.
이미지 없음 (No-Image)
images가 빈 배열이거나, 항목의 src가 비었거나, 이미지 로드에 실패하면 대체 표시가 나옵니다. fallbackSrc를 지정하면 그 이미지를(alt는 labels.noImage), 없으면 아이콘을 보입니다. 로드 중에는 스켈레톤이 보입니다.
Preview
API Reference
CarouselImage
| 필드 | Type | 필수 | Description |
|---|---|---|---|
id | string | ✅ | 슬라이드 key |
src | string | ✅ | 이미지 URL |
alt | string | - | 대체 텍스트 (접근성) |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | 슬라이드(Carousel.Item). images보다 이김 |
images | T[] (T extends CarouselImage) | [] | 표시할 이미지 목록. 항목에 필드를 더 실을 수 있음. 비어 있으면 대체 표시 |
autoplay | boolean | false | 자동 재생. 2장 이상이고 움직임 줄이기가 꺼져 있을 때만. 마우스·포커스가 들어오면 멈춤 |
delay | number | 4000 | 자동 재생 간격 (ms) |
arrows | "always" | "hover" | "never" | "always" | 이전·다음 화살표. always 늘 보임, hover 마우스·키보드 포커스 때만, never 그리지 않음 |
indicators | boolean | true | 슬라이드 수만큼 점을 그려 현재 위치를 보이고, 점을 누르면 그 슬라이드로 이동 |
height | string | number | 300px | 캐러셀 전체 높이 (number → px, 인디케이터 포함). 미지정이면 className(예: h-full)·style로 덮을 수 있음 |
fallbackSrc | string | - | 이미지가 없을 때(빈 images) 보일 대체 이미지 URL. 없으면 아이콘 |
opts | CarouselOptions | { loop: true, watchDrag: 2장 이상 } | embla-carousel 옵션 객체 (loop, watchDrag, startIndex, align 등). 넘긴 값이 이김 |
labels | Partial<CarouselLabels> | 영어 기본값 | 화면 문구 (carousel, noImage, slide, slideOf). 위 표 참고 |
plugins | CarouselPlugin | - | embla 플러그인 배열 |
setApi | (api: CarouselApi) => void | - | embla api를 받는 콜백 |
| (그 밖) | foundation CarouselProps | - | 캐러셀 영역의 aria-label·id·이벤트를 그대로 전달. orientation은 받지 않음(가로만) |
className | string | - | 루트 className |
style | CSSProperties | - | 루트 인라인 스타일 |
ref | Ref<CarouselRef> | - | currentIndex/currentImage/scroll 제어 |
Events
| Event | Type | Description |
|---|---|---|
onImageClick | (data: CarouselClickData) => void | 이미지 클릭 시 호출 |
onSlideChange | (data: CarouselChangeData) => void | 슬라이드 변경 시 호출 |
CarouselRef
| 멤버 | Type | Description |
|---|---|---|
currentIndex | number | 현재 슬라이드 인덱스 |
currentImage | CarouselImage | undefined | 현재 이미지 정보 |
scrollTo(index) | (index: number) => void | 지정 슬라이드로 이동 |
scrollNext() | () => void | 다음 슬라이드 |
scrollPrev() | () => void | 이전 슬라이드 |
element | HTMLDivElement | null | 컨테이너 DOM 요소 |
접근성
권장 사항
- ✅ 각 이미지에
alt텍스트를 제공하여 대체 텍스트를 노출 - ✅ 캐러셀 영역에
role="region",aria-roledescription="carousel"부여 (Foundation 기본) - ✅ 각 슬라이드에
role="group",aria-roledescription="slide"부여 (Foundation 기본) - ✅ 인디케이터에
aria-label과aria-current로 현재 슬라이드를 표시
관련 컴포넌트
- ImageView: 확대 보기를 지원하는 이미지 뷰어
- Foundation Carousel: 기본 캐러셀 컴포넌트