Carousel

Embla based carousel component
Import
License

Installation

Package depends on @mantine/core and @mantine/hooks.

Install with yarn:

yarn add embla-carousel-react @mantine/carousel

Install with npm:

npm install embla-carousel-react @mantine/carousel

Usage

@mantine/carousel package is based on embla carousel, it supports most of its features:

import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel sx={{ maxWidth: 320 }} mx="auto" withIndicators height={200}>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

Options

Align
Orientation
SlideGap
xs
sm
md
lg
xl
ControlsOffset
xs
sm
md
lg
xl
import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel slideSize="70%" height={200} slideGap="md">
{/* ...slides */}
</Carousel>
);
}

Size and gap

Set slideSize and slideGap on Carousel component to control size and gap of every slide:

import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
withIndicators
height={200}
slideSize="33.333333%"
slideGap="md"
loop
align="start"
slidesToScroll={3}
>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

You can also control size and gap of each slide individually by setting size and gap props on Carousel.Slide component:

import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel>
<Carousel.Slide size="50%" gap="xl">
1
</Carousel.Slide>
<Carousel.Slide size="100%">2</Carousel.Slide>
<Carousel.Slide size={200} gap={20}>
3
</Carousel.Slide>
</Carousel>
);
}

Responsive styles

breakpoints prop works the same way as in SimpleGrid component. You can use maxWidth or minWidth to define slideSize and slideGap at given breakpoints:

import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
withIndicators
height={200}
slideSize="33.333333%"
slideGap="md"
breakpoints={[
{ maxWidth: 'md', slideSize: '50%' },
{ maxWidth: 'sm', slideSize: '100%', slideGap: 0 },
]}
loop
align="start"
>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

Drag free

dragFree will disable slides snap points – user will be able to stop dragging at any position:

import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
sx={{ maxWidth: 320 }}
mx="auto"
withIndicators
height={200}
dragFree
slideGap="md"
align="start"
>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

Vertical orientation

Carousel with orientation="vertical" requires height prop to be set:

import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel orientation="vertical" height={200} withIndicators sx={{ maxWidth: 320 }} mx="auto">
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

Controls icons

You can replace default next/previous controls icons with any React nodes:

import { Carousel } from '@mantine/carousel';
import { IconArrowRight, IconArrowLeft } from '@tabler/icons';
function Demo() {
return (
<Carousel
sx={{ maxWidth: 320 }}
mx="auto"
height={180}
nextControlIcon={<IconArrowRight size={16} />}
previousControlIcon={<IconArrowLeft size={16} />}
>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

100% height

Set height="100%" to make Carousel take 100% height of the container. Note that in this case:

  • container element must have display: flex styles
  • carousel root element must have flex: 1 styles
  • container element must have fixed height
import { Carousel } from '@mantine/carousel';
export function PercentageHeight() {
return (
<div style={{ height: 400, display: 'flex' }}>
<Carousel withIndicators height="100%" sx={{ flex: 1 }}>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
</Carousel>
</div>
);
}

Get embla instance

You can get embla instance with getEmblaApi prop. You will be able enhance carousel with additional logic after that using embla api methods:

import { useCallback, useEffect, useState } from 'react';
import { Carousel, Embla } from '@mantine/carousel';
import { Progress } from '@mantine/core';
function Demo() {
const [scrollProgress, setScrollProgress] = useState(0);
const [embla, setEmbla] = useState<Embla | null>(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
dragFree
slideSize="50%"
slideGap="md"
height={200}
getEmblaApi={setEmbla}
initialSlide={2}
>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
<Progress
value={scrollProgress}
styles={{ bar: { transitionDuration: '0ms' }, root: { maxWidth: 320 } }}
size="sm"
mt="xl"
mx="auto"
/>
</>
);
}

Embla plugins

Set plugins prop to enhance carousel with embla plugins. Note that plugins are not installed with @mantine/carousel package and you will need to install them on your side.

Example with autoplay plugin:

import { useRef } from 'react';
import Autoplay from 'embla-carousel-autoplay';
import { Carousel } from '@mantine/carousel';
function Demo() {
const autoplay = useRef(Autoplay({ delay: 2000 }));
return (
<Carousel
sx={{ maxWidth: 320 }}
mx="auto"
withIndicators
height={200}
plugins={[autoplay.current]}
onMouseEnter={autoplay.current.stop}
onMouseLeave={autoplay.current.reset}
>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

Styles API

Carousel supports Styles API, you can customize styles of any inner element.

Indicator styles

import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
sx={{ maxWidth: 320 }}
mx="auto"
withIndicators
height={200}
styles={{
indicator: {
width: 12,
height: 4,
transition: 'width 250ms ease',
'&[data-active]': {
width: 40,
},
},
}}
>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

Hide inactive controls

import { Carousel } from '@mantine/carousel';
function Demo() {
return (
<Carousel
sx={{ maxWidth: 320 }}
mx="auto"
height={200}
styles={{
control: {
'&[data-inactive]': {
opacity: 0,
cursor: 'default',
},
},
}}
>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

Show controls on hover

import { createStyles } from '@mantine/core';
import { Carousel } from '@mantine/carousel';
const useStyles = createStyles((_theme, _params, getRef) => ({
controls: {
ref: getRef('controls'),
transition: 'opacity 150ms ease',
opacity: 0,
},
root: {
'&:hover': {
[`& .${getRef('controls')}`]: {
opacity: 1,
},
},
},
}));
function Demo() {
const { classes } = useStyles();
return (
<Carousel sx={{ maxWidth: 320 }} mx="auto" height={200} classNames={classes}>
<Carousel.Slide>1</Carousel.Slide>
<Carousel.Slide>2</Carousel.Slide>
<Carousel.Slide>3</Carousel.Slide>
{/* ...other slides */}
</Carousel>
);
}

Examples

Images carousel

Cards carousel

Carousel container animation offset

Embla carousel only reads slides positions and sizes upon initialization. When you are using Carousel component inside animated component you may experience an issue with incorrect slides offset after animation finishes.

Example of incorrect slides offset calculation (scroll though slides):

To solve this issue use useAnimationOffsetEffect hook exported from @mantine/carousel package. It accepts embla instance as first argument and transition duration as second:

import { useState } from 'react';
import { Button, Modal, Group } from '@mantine/core';
import { Carousel, useAnimationOffsetEffect } from '@mantine/carousel';
function Demo() {
const TRANSITION_DURATION = 200;
const [opened, setOpened] = useState(false);
const [embla, setEmbla] = useState<Embla | null>(null);
useAnimationOffsetEffect(embla, TRANSITION_DURATION);
return (
<>
<Group position="center">
<Button onClick={() => setOpened(true)}>Open modal with carousel</Button>
</Group>
<Modal
opened={opened}
size="300px"
padding={0}
transitionDuration={TRANSITION_DURATION}
withCloseButton={false}
onClose={() => setOpened(false)}
>
<Carousel loop getEmblaApi={setEmbla}>
<Carousel.Slide>
<img
src="https://cataas.com/cat"
alt=""
style={{ width: 300, height: 200, objectFit: 'cover' }}
/>
</Carousel.Slide>
<Carousel.Slide>
<img
src="https://cataas.com/cat/cute"
alt=""
style={{ width: 300, height: 200, objectFit: 'cover' }}
/>
</Carousel.Slide>
<Carousel.Slide>
<img
src="https://cataas.com/cat/angry"
alt=""
style={{ width: 300, height: 200, objectFit: 'cover' }}
/>
</Carousel.Slide>
</Carousel>
</Modal>
</>
);
}

Carousel component props

NameTypeDescription
align
number | "center" | "end" | "start"
Determines how slides will be aligned relative to the container. Use number between 0-1 to align slides based on percentage, where 0.5 equals 50%
breakpoints
CarouselBreakpoint[]
Control slideSize and slideGap at different viewport sizes
children
ReactNode
<Carousel.Slide /> components
containScroll
"" | "trimSnaps" | "keepSnaps"
Clear leading and trailing empty space that causes excessive scrolling. Use trimSnaps to only use snap points that trigger scrolling or keepSnaps to keep them.
controlSize
number
Previous/next controls size in px
controlsOffset
number | "xs" | "sm" | "md" | "lg" | "xl"
Key of theme.spacing or number to set space between next/previous control and carousel boundary
dragFree
boolean
Determines whether momentum scrolling should be enabled, false by default
draggable
boolean
Determines whether carousel can be scrolled with mouse and touch interactions, true by default
getEmblaApi
(embla: EmblaCarouselType) => void
Get embla API as ref
height
Height<string | number>
Slides container height, required for vertical orientation
inViewThreshold
number
Choose a fraction representing the percentage portion of a slide that needs to be visible in order to be considered in view. For example, 0.5 equals 50%.
includeGapInSize
boolean
Determines whether gap should be treated as part of the slide size, true by default
initialSlide
number
Index of initial slide
loop
boolean
Enables infinite looping. Automatically falls back to false if slide content isn't enough to loop.
nextControlIcon
ReactNode
Icon of next control
nextControlLabel
string
Next control aria-label
onNextSlide
() => void
Called when user clicks next button
onPreviousSlide
() => void
Called when user clicks previous button
onSlideChange
(index: number) => void
Called with slide index when slide changes
orientation
"horizontal" | "vertical"
Carousel orientation, horizontal by default
plugins
CreatePluginType<LoosePluginType, {}>[]
An array of embla plugins
previousControlIcon
ReactNode
Previous control icon
previousControlLabel
string
Previous control aria-label
skipSnaps
boolean
Allow the carousel to skip scroll snaps if it's dragged vigorously. Note that this option will be ignored if the dragFree option is set to true, false by default
slideGap
number | "xs" | "sm" | "md" | "lg" | "xl"
Key of theme.spacing or number to set gap between slides in px
slideSize
string | number
Slide width, defaults to 100%, examples: 200px, 50%
slidesToScroll
number | "auto"
Number of slides that should be scrolled with next/previous buttons
speed
number
Adjusts scroll speed when triggered by any of the methods. Higher numbers enables faster scrolling.
withControls
boolean
Determines whether next/previous controls should be displayed, true by default
withIndicators
boolean
Determines whether indicators should be displayed, false by default
withKeyboardEvents
boolean
Determines whether arrow key should switch slides, true by default

Carousel.Slide component props

NameTypeDescription
children
ReactNode
Slide content
gap
number | "xs" | "sm" | "md" | "lg" | "xl"
Key of theme.spacing or number to set gap between slides in px
size
string | number
Slide width, defaults to 100%, examples: 200px, 50%

Carousel component Styles API

NameStatic selectorDescription
root.mantine-Carousel-rootRoot element
slide.mantine-Carousel-slideSlide root element
container.mantine-Carousel-containerSlides container
viewport.mantine-Carousel-viewportMain element, contains slides container and all controls
controls.mantine-Carousel-controlsNext/previous controls container
control.mantine-Carousel-controlNext/previous control
indicators.mantine-Carousel-indicatorsIndicators container
indicator.mantine-Carousel-indicatorIndicator button