@mantine-bites/lightbox
Full-screen image lightbox with thumbnails, controls, and carousel navigation
Installation
yarn add embla-carousel@^8.5.2 embla-carousel-react@^8.5.2 @mantine-bites/lightboxnpm install embla-carousel@^8.5.2 embla-carousel-react@^8.5.2 @mantine-bites/lightboxpnpm add embla-carousel@^8.5.2 embla-carousel-react@^8.5.2 @mantine-bites/lightboxAfter installation import package styles at the root of your application:
import '@mantine/core/styles.css';
// ‼️ import lightbox styles after core package styles
import '@mantine-bites/lightbox/styles.css';Usage
@mantine-bites/lightbox is built on top of embla-carousel.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide }}
/>
</>
);
}Transition
Customize the open/close animation using transitionProps. Props are passed to the Mantine Transition component. Defaults to transition: 'fade' and duration: 250.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide }}
transitionProps={{ transition: 'slide-up', duration: 400 }}
/>
</>
);
}Overlay
Customize the backdrop using overlayProps. Props are passed to the Mantine Overlay component. Defaults to color: '#18181B', backgroundOpacity: 0.9 and zIndex: 200.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide }}
overlayProps={{ color: '#ADD8E6', backgroundOpacity: 0.7 }}
/>
</>
);
}Slides
Use slidesProps to configure the main slides carousel. Supports initialSlide, emblaOptions, and emblaPlugins.
See the Embla options documentation for all available options.
Thumbnails
Use thumbnailsProps to configure the thumbnail strip carousel. Currently defaults to dragFree: true.
See the Embla options documentation for all available options.
Controls
Use controlsProps to configure the prev/next navigation buttons. size controls button size in px. Currently defaults to 36.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide }}
controlsProps={{ size: 48 }}
/>
</>
);
}Counter
Use counterProps to customize the slide counter. The formatter function receives the current zero-based index and the total slide count.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide }}
counterProps={{ formatter: (index, total) => `Image ${index + 1} of ${total}` }}
/>
</>
);
}Embla options
Pass options directly to the Embla carousel instance via slidesProps.emblaOptions.
See the Embla options documentation for all available options.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide, emblaOptions: { loop: true } }}
/>
</>
);
}Embla plugins
Pass plugins to the main slides carousel via slidesProps.emblaPlugins.
See the Embla plugins documentation for more information.
If the autoplay plugin is detected, an autoplay toggle button is automatically added to the toolbar.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import Autoplay from 'embla-carousel-autoplay';
import { useState } from 'react';
const autoplay = Autoplay();
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide, emblaPlugins: [autoplay] }}
/>
</>
);
}Vertical orientation
Set orientation="vertical" to switch the slides carousel to a vertical layout.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide }}
orientation="vertical"
/>
</>
);
}Mantine Image
The <Lightbox /> wrapper uses Mantine Image under the hood.
This allows you to pass Mantine props directly in slideImageProps and thumbnailImageProps.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox
images={images}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide }}
slideImageProps={{
radius: "50%",
}}
thumbnailImageProps={{
radius: "50%",
}}
/>
</>
);
}Next.js Image
You can pass next/image to slideImageProps.component or slideImageProps.renderRoot.
I recommend including the width and height properties in your images array when using next/image. Alternatively if you do not know these values, you can use renderRoot and pass in fill={true}, however this will cause smaller images to increase to the size of the lightbox viewport, reducing the quality of the image.
Refer to Next.js image documentation for more information.
import { SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import NextImage from 'next/image';
import {
// type ComponentProps,
useState,
} from 'react';
const images = [
{
src: "https://picsum.photos/id/10/2400/1600",
alt: "Forest",
width: 2400,
height: 1600,
},
{
src: "https://picsum.photos/id/20/1200/800",
alt: "Books",
width: 1200,
height: 800,
},
{
src: "https://picsum.photos/id/30/2400/1600",
alt: "Mug",
width: 2400,
height: 1600,
},
{
src: "https://picsum.photos/id/40/1200/800",
alt: "Cat",
width: 1200,
height: 800,
},
{
src: "https://picsum.photos/id/50/2400/1600",
alt: "Bird",
width: 2400,
height: 1600,
},
{
src: "https://picsum.photos/id/60/1200/800",
alt: "Computer",
width: 1200,
height: 800,
},
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<div
key={img.src}
style={{ position: "relative", aspectRatio: "3/2" }}
>
<NextImage
src={img.src}
alt={img.alt}
fill
style={{ objectFit: "cover", borderRadius: 8 }}
onClick={() => open(index)}
/>
</div>
))}
</SimpleGrid>
<Lightbox
images={images}
slideImageProps={{
component: NextImage,
// Use renderRoot if you need more fine-grained control and better type-safety
// renderRoot: (props: ComponentProps<typeof NextImage>) => (
// <NextImage {...props} />
// ),
}}
opened={opened}
onClose={() => setOpened(false)}
slidesProps={{ initialSlide }}
/>
</>
);
}Compound components
For full control over layout and composition, use the compound components via Lightbox.Root.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox.Root opened={opened} onClose={() => setOpened(false)}>
<Lightbox.Toolbar />
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides initialSlide={initialSlide}>
{images.map((img) => (
<Lightbox.Slide key={img.src}>
<img src={img.src} alt={img.alt} />
</Lightbox.Slide>
))}
</Lightbox.Slides>
<Lightbox.Thumbnails>
{images.map((img) => (
<Lightbox.Thumbnail key={img.src}>
<img src={img.src} alt={img.alt} />
</Lightbox.Thumbnail>
))}
</Lightbox.Thumbnails>
</Lightbox.Root>
</>
);
}Compound component example - custom toolbar button
Pass children to Lightbox.Toolbar to compose a custom set of buttons.
import { ActionIcon, Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { IconDownload } from '@tabler/icons-react';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image
key={img.src}
src={img.src}
alt={img.alt}
radius="md"
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox.Root opened={opened} onClose={() => setOpened(false)}>
<Lightbox.Toolbar>
<Lightbox.ZoomButton />
<Lightbox.FullscreenButton />
<ActionIcon variant="default" size="lg" aria-label="Download">
<IconDownload />
</ActionIcon>
<Lightbox.CloseButton />
</Lightbox.Toolbar>
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides initialSlide={initialSlide}>
{images.map((img) => (
<Lightbox.Slide key={img.src}>
<img src={img.src} alt={img.alt} />
</Lightbox.Slide>
))}
</Lightbox.Slides>
<Lightbox.Thumbnails>
{images.map((img) => (
<Lightbox.Thumbnail key={img.src}>
<img src={img.src} alt={img.alt} />
</Lightbox.Thumbnail>
))}
</Lightbox.Thumbnails>
</Lightbox.Root>
</>
);
}Compound component example - custom footer
Integrate your own custom components inside the compound component architecture to suit your needs.
import { Box, Image, SimpleGrid, Text } from '@mantine/core';
import { Lightbox, useLightboxContext } from '@mantine-bites/lightbox';
import { useState } from 'react';
const images = [
{ src: "https://picsum.photos/id/10/2400/1600", alt: "Forest" },
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books" },
{ src: "https://picsum.photos/id/30/2400/1600", alt: "Mug" },
{ src: "https://picsum.photos/id/40/1200/800", alt: "Cat" },
{ src: "https://picsum.photos/id/50/2400/1600", alt: "Bird" },
{ src: "https://picsum.photos/id/60/1200/800", alt: "Computer" },
];
function LightboxFooter({ images }) {
const { currentIndex } = useLightboxContext();
const image = images[currentIndex];
return (
<Box bg="blue.7" w="100%" p="xs">
<Text size="sm" c="white" ta="center">
{image?.alt ?? ''}
</Text>
</Box>
);
}
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 2, sm: 3 }}>
{images.map((img, index) => (
<Image key={img.src} src={img.src} alt={img.alt} radius="md" onClick={() => open(index)} />
))}
</SimpleGrid>
<Lightbox.Root opened={opened} onClose={() => setOpened(false)}>
<Lightbox.Toolbar />
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides initialSlide={initialSlide}>
{images.map((img) => (
<Lightbox.Slide key={img.src}>
<img src={img.src} alt={img.alt} />
</Lightbox.Slide>
))}
</Lightbox.Slides>
<Lightbox.Thumbnails>
{images.map((img) => (
<Lightbox.Thumbnail key={img.src}>
<img src={img.src} alt={img.alt} />
</Lightbox.Thumbnail>
))}
</Lightbox.Thumbnails>
<LightboxFooter images={images} />
</Lightbox.Root>
</>
);
}