Carousel
Carousel
Embla 기반 캐러셀 컴포넌트예요. @mantine/carousel 패키지로 제공되며 MIT 라이선스로 배포돼요.
출처: 문서
본문
설치
yarn add embla-carousel@^8.5.2 embla-carousel-react@^8.5.2 @mantine/carousel
설치 후 애플리케이션의 루트에서 패키지 스타일을 import 해요:
import '@mantine/core/styles.css';
// ‼️ carousel 스타일은 core 패키지 스타일 다음에 import 하세요
import '@mantine/carousel/styles.css';
스타일 import를 잊지 마세요
위의 설치 안내를 따랐는데도 뭔가 동작하지 않는다면(Carousel 슬라이드가 세로로 렌더링되거나, 컨트롤이나 인디케이터가 없다면) carousel 스타일을 import 하지 않은 함정에 빠진 거예요! 문제를 해결하려면 애플리케이션 루트에 carousel 스타일을 import 해요:
import '@mantine/carousel/styles.css';
문서 데모
이 페이지의 데모는 시연 목적으로 파란 배경색을 사용해요. 데모 코드를 단순화하기 위해 배경색과 기타 데모 전용 스타일은 데모 코드에 포함되지 않아요. 데모 코드를 프로젝트에 복사-붙여넣기하면 파란 배경색이 없을 거예요.
사용법
@mantine/carousel 패키지는 embla carousel을 기반으로 해요:
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel height={200}>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}
옵션
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
orientation="horizontal"
slideGap="md"
controlsOffset="xs"
controlSize={32}
withControls
withIndicators
>
{/* ...slides */}
</Carousel>
);
}
Embla 옵션
emblaOptions prop으로 embla carousel에 구성 옵션을 직접 전달할 수 있어요. embla 옵션 설명은 embla options reference에서 찾을 수 있어요.
loop, dragFree, align 옵션을 전달하는 예시:
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
height={200}
slideSize="70%"
slideGap="md"
loop
dragFree
align="start"
>
{/* ...slides */}
</Carousel>
);
}
크기와 간격
Carousel 컴포넌트에 slideSize와 slideGap을 설정해 모든 슬라이드의 크기와 간격을 제어해요:
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
height={200}
slideSize="25%"
slideGap="md"
>
1
2
3
{/* ...other slides */}
</Carousel>
);
}
반응형 스타일
slideSize와 slideGap props는 style props와 같은 방식으로 동작하며, 서로 다른 breakpoint 값이 담긴 객체를 전달할 수 있어요:
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
height={200}
slideSize={{ base: '100%', sm: '50%', md: '33.333333%' }}
slideGap={{ base: 0, sm: 'md' }}
>
1
2
3
{/* ...other slides */}
</Carousel>
);
}
Container queries
미디어 쿼리 대신 container queries를 사용하려면 type="container"로 설정해요. Container queries를 사용하면 slide 크기와 간격이 뷰포트 너비가 아닌 컨테이너 너비에 맞춰 조정돼요.
Container queries를 사용할 때 slideSize와 slideGap props의 키에서 theme.breakpoints 값을 참조할 수 없다는 점에 주의하세요. 정확한 px 또는 em 값을 사용해야 해요.
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
// Wrapper div is added for demonstration purposes only,
// It is not required in real projects
<div style={{ resize: 'horizontal', overflow: 'auto', padding: 24 }}>
<Carousel type="container" height={200} slideSize="50%" slideGap="md">
1
2
3
{/* ...other slides */}
</Carousel>
</div>
);
}
Drag free
dragFree는 슬라이드 snap point를 비활성화해요 – 사용자가 어느 위치에서든 드래그를 멈출 수 있어요:
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel height={200} slideSize="25%" slideGap="md" dragFree>
1
2
3
{/* ...other slides */}
</Carousel>
);
}
세로 방향
orientation="vertical" 캐러셀은 height prop을 설정해야 해요:
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel orientation="vertical" height={260} slideSize="50%" slideGap="md">
1
2
3
{/* ...other slides */}
</Carousel>
);
}
컨트롤 아이콘
기본 next/previous 컨트롤 아이콘을 어떤 React 노드로든 교체할 수 있어요:
import { Carousel } from '@mantine/carousel';
import { ArrowRightIcon, ArrowLeftIcon } from '@phosphor-icons/react';
function Demo() {
return (
<Carousel
height={200}
slideSize="25%"
slideGap="md"
nextControlIcon={<ArrowRightIcon />}
previousControlIcon={<ArrowLeftIcon />}
>
1
2
3
{/* ...other slides */}
</Carousel>
);
}
100% 높이
height="100%"로 설정하면 Carousel이 컨테이너의 100% 높이를 차지해요. 이 경우 다음에 주의하세요:
-
컨테이너 요소는
display: flex스타일을 가져야 해요 -
캐러셀 루트 요소는
flex: 1스타일을 가져야 해요 -
컨테이너 요소는 고정 높이를 가져야 해요
import { Carousel } from '@mantine/carousel';
export function PercentageHeight() {
return (
<div style={{ display: 'flex', height: 300 }}>
<Carousel style={{ flex: 1 }} slideSize="100%">
1
2
3
</Carousel>
</div>
);
}
Embla 인스턴스 가져오기
getEmblaApi prop으로 embla instance를 얻을 수 있어요. 그러면 embla api 메서드를 사용해 캐러셀에 추가 로직을 더할 수 있어요:
import { useCallback, useEffect, useState } from 'react';
import { EmblaCarouselType } from 'embla-carousel';
import { Carousel } from '@mantine/carousel';
import { Progress } from '@mantine/core';
function Demo() {
const [scrollProgress, setScrollProgress] = useState(0);
const [embla, setEmbla] = useState(null);
const handleScroll = useCallback(() => {
if (!embla) {
return;
}
const progress = Math.max(0, Math.min(1, embla.scrollProgress()));
setScrollProgress(progress * 100);
}, [embla, setScrollProgress]);
useEffect(() => {
if (embla) {
embla.on('scroll', handleScroll);
handleScroll();
}
}, [embla]);
return (
<>
<Carousel
withControls={false}
height={200}
slideSize="25%"
slideGap="md"
getEmblaApi={setEmbla}
>
1
2
3
{/* ...other slides */}
</Carousel>
<Progress value={scrollProgress} mt="md" />
</>
);
}
Embla 플러그인
캐러셀을 embla plugins으로 향상시키려면 plugins prop을 설정해요. 플러그인은 @mantine/carousel 패키지와 함께 설치되지 않으므로 별도로 설치해야 한다는 점에 주의하세요.
autoplay plugin 예시:
yarn add embla-carousel-autoplay@^8.5.2
import { useRef } from 'react';
import Autoplay from 'embla-carousel-autoplay';
import { Carousel } from '@mantine/carousel';
function Demo() {
const autoplay = useRef(Autoplay({ delay: 1000 }));
return (
<Carousel
height={200}
plugins={[autoplay.current]}
onMouseEnter={autoplay.current.stop}
onMouseLeave={autoplay.current.reset}
>
1
2
3
{/* ...other slides */}
</Carousel>
);
}
Styles API
Carousel은 Styles API를 지원해요; classNames prop으로 컴포넌트의 어떤 내부 요소에든 스타일을 추가할 수 있어요. 자세한 내용은 Styles API 문서를 참고하세요.
| Selector | 설명 |
|---|---|
| root | 루트 요소 |
| slide | Carousel.Slide 루트 요소 |
| container | 슬라이드 컨테이너 |
| viewport | 슬라이드 컨테이너와 모든 컨트롤을 포함하는 메인 요소 |
| controls | next/previous 컨트롤 컨테이너 |
| control | next/previous 컨트롤 |
| indicators | 인디케이터 컨테이너 |
| indicator | 인디케이터 버튼 |
인디케이터 스타일
import { Carousel } from '@mantine/carousel';
import classes from './Demo.module.css';
function Demo() {
return (
<Carousel
height={200}
slideSize="25%"
slideGap="md"
classNames={{ indicator: classes.indicator }}
>
1
2
3
{/* ...other slides */}
</Carousel>
);
}
비활성 컨트롤 숨기기
import { Carousel } from '@mantine/carousel';
import classes from './Demo.module.css';
function Demo() {
return (
<Carousel
height={200}
slideSize="25%"
slideGap="md"
withIndicators
classNames={classes}
>
1
2
3
{/* ...other slides */}
</Carousel>
);
}
hover 시 컨트롤 표시
import { Carousel } from '@mantine/carousel';
import classes from './Demo.module.css';
function Demo() {
return (
<Carousel
height={200}
slideSize="25%"
slideGap="md"
withIndicators
classNames={classes}
>
1
2
3
{/* ...other slides */}
</Carousel>
);
}
예시: 이미지 캐러셀
import { Carousel } from '@mantine/carousel';
import { Image } from '@mantine/core';
const images = [
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png',
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png',
];
function Demo() {
const slides = images.map((url) => (
<Carousel.Slide key={url}>
<Image src={url} />
</Carousel.Slide>
));
return (
<Carousel slideSize="70%" slideGap="md" height={200} loop withIndicators>
{slides}
</Carousel>
);
}
예시: 카드 캐러셀
import { Carousel } from '@mantine/carousel';
import { useMediaQuery } from '@mantine/hooks';
import { Button, Paper, Title, useMantineTheme, Text } from '@mantine/core';
import classes from './Demo.module.css';
const data = [
{
image:
'https://images.unsplash.com/photo-1508193638397-1c4234db14d8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=400&q=80',
title: 'Best forests to visit in North America',
category: 'nature',
},
{
image:
'https://images.unsplash.com/photo-1559494007-9f5847c49d94?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=400&q=80',
title: 'Hawaii beaches review: better than you think',
category: 'beach',
},
{
image:
'https://images.unsplash.com/photo-1608481337062-4093bf3ed404?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=400&q=80',
title: 'Mountains at night: 12 best locations to enjoy the view',
category: 'nature',
},
];
interface CardProps {
image: string;
title: string;
category: string;
}
function Card({ image, title, category }: CardProps) {
return (
<Paper shadow="md" p="xl" radius="md">
<Text tt="uppercase" fw={700} c="dimmed">
{category}
</Text>
<Title order={3} my="sm">
{title}
</Title>
<Button variant="light" color="blue" fullWidth>
Read article
</Button>
</Paper>
);
}
function Demo() {
const theme = useMantineTheme();
const mobile = useMediaQuery(`(max-width: ${theme.breakpoints.sm})`);
const slides = data.map((item) => (
<Carousel.Slide key={item.title}>
<Card {...item} />
</Carousel.Slide>
));
return (
<Carousel
slideSize={mobile ? '100%' : '50%'}
slideGap="md"
align="start"
>
{slides}
</Carousel>
);
}
접근성
Carousel 컴포넌트에 aria-label 또는 aria-labelledby를 설정해 스크린 리더가 접근 가능하게 해요:
import { Carousel } from '@mantine/carousel';
export function AccessibleCarousel() {
return (
<Carousel aria-label="Images carousel" height={200}>
...
</Carousel>
);
}
nextControlProps와 previousControlProps props로 next/previous 컨트롤에 aria-label을 설정해요:
import { Carousel } from '@mantine/carousel';
export function AccessibleControlsCarousel() {
return (
<Carousel
height={200}
nextControlProps={{ 'aria-label': 'Next slide' }}
previousControlProps={{ 'aria-label': 'Previous slide' }}
>
...
</Carousel>
);
}