@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/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)}
initialSlide={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)}
initialSlide={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)}
initialSlide={initialSlide}
overlayProps={{ color: '#ADD8E6', backgroundOpacity: 0.7 }}
/>
</>
);
}Slides
Use slidesProps to configure the main slides carousel with 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)}
initialSlide={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)}
initialSlide={initialSlide}
counterProps={{ formatter: (index, total) => `Image ${index + 1} of ${total}` }}
/>
</>
);
}Caption
Add a caption to any image in the images array to display text below the slide. Caption text is selectable and does not trigger carousel drag or close the lightbox when clicked.
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",
caption: (
<>
A peaceful forest scene
<br />
<em>Photographed in the Pacific Northwest</em>
</>
),
},
{
src: "https://picsum.photos/id/20/1200/800",
alt: "Books",
caption: "A stack of books",
},
{
src: "https://picsum.photos/id/30/2400/1600",
alt: "Mug",
caption: "Coffee break",
},
{
src: "https://picsum.photos/id/40/1200/800",
alt: "Cat",
caption: "A curious 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)}
initialSlide={initialSlide}
/>
</>
);
}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)}
initialSlide={initialSlide}
slidesProps={{ emblaOptions: { loop: true } }}
/>
</>
);
}Embla plugins
Pass plugins to the main slides carousel via slidesProps.emblaPlugins.
See the Embla plugins documentation for more information.
Autoplay
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)}
initialSlide={initialSlide}
slidesProps={{ emblaPlugins: [autoplay] }}
/>
</>
);
}Fade
The fade plugin replaces the default scroll transition between slides with a crossfade.
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import Fade from 'embla-carousel-fade';
import { useState } from 'react';
const fade = Fade();
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)}
initialSlide={initialSlide}
slidesProps={{ emblaPlugins: [fade] }}
/>
</>
);
}Embla API
Use getEmblaApi on slidesProps (convenience component) or directly on <Lightbox.Slides> (compound component) to access the Embla carousel API. This is useful for subscribing to carousel events and issuing programmatic commands not exposed by the lightbox itself.
import { Box, Button, Group, Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useCallback, useRef, 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 emblaApiRef = useRef(null);
const progressBarRef = useRef(null);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
const handleEmblaApi = useCallback((embla) => {
emblaApiRef.current = embla;
const onScroll = (api) => {
if (progressBarRef.current) {
progressBarRef.current.style.width =
Math.max(0, api.scrollProgress()) * 100 + '%';
}
};
embla.on('scroll', onScroll);
embla.on('reInit', onScroll);
onScroll(embla);
}, []);
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)}
initialSlide={initialSlide}
>
<Lightbox.Toolbar />
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides getEmblaApi={handleEmblaApi}>
{images.map((img) => (
<Lightbox.Slide key={img.src}>
<img src={img.src} alt={img.alt} />
</Lightbox.Slide>
))}
</Lightbox.Slides>
<Group w="100%" px="md" py="xs" gap="sm">
<Button
variant="subtle"
c="white"
size="xs"
onClick={() => emblaApiRef.current?.scrollTo(0)}
>
First
</Button>
<Box
flex={1}
h={8}
bg="dark.5"
style={{ borderRadius: 4, overflow: 'hidden' }}
>
<div
ref={progressBarRef}
style={{
height: '100%',
width: '0%',
backgroundColor: 'var(--mantine-primary-color-filled)',
borderRadius: 'inherit',
}}
/>
</Box>
<Button
variant="subtle"
c="white"
size="xs"
onClick={() => emblaApiRef.current?.scrollTo(images.length - 1)}
>
Last
</Button>
</Group>
</Lightbox.Root>
</>
);
}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)}
initialSlide={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)}
initialSlide={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)}
initialSlide={initialSlide}
/>
</>
);
}Custom content
The compound component API supports any React content inside <Lightbox.Slide> - not just images. For non-image content, set withZoom={false} on Lightbox.Root to hide the zoom button (which requires an image to function).
HTML content
Slides can contain any HTML or React content - useful for rich text, profiles, articles, or any custom UI.
Alice Chen
Engineering Lead
Marco Rossi
Design Systems
Priya Nair
Product Manager
import {
Badge,
Box,
Card,
Divider,
Group,
SimpleGrid,
Text,
Title,
} from "@mantine/core";
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const members = [
{
name: "Alice Chen",
role: "Engineering Lead",
skills: ["TypeScript", "React", "Node.js"],
bio: "Alice has 10 years of experience building scalable web applications...",
},
{
name: "Marco Rossi",
role: "Design Systems",
skills: ["Figma", "CSS", "Accessibility"],
bio: "Marco specialises in design systems and component libraries...",
},
{
name: "Priya Nair",
role: "Product Manager",
skills: ["Strategy", "Analytics", "User Research"],
bio: "Priya bridges the gap between user needs and technical capabilities...",
},
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index: number) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 1, sm: 3 }}>
{members.map((member, index) => (
<Card
key={member.name}
withBorder
radius="md"
style={{ cursor: "pointer" }}
onClick={() => open(index)}
>
<Text fw={600}>{member.name}</Text>
<Text size="sm" c="dimmed">
{member.role}
</Text>
</Card>
))}
</SimpleGrid>
<Lightbox.Root
opened={opened}
onClose={() => setOpened(false)}
initialSlide={initialSlide}
withZoom={false}
>
<Lightbox.Toolbar />
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides>
{members.map((member) => (
<Lightbox.Slide key={member.name}>
<Box p="xl" maw={560} w="100%">
<Title order={2}>{member.name}</Title>
<Text size="lg" c="dimmed" mb="md">
{member.role}
</Text>
<Divider mb="md" />
<Text mb="lg">{member.bio}</Text>
<Group gap="xs">
{member.skills.map((skill) => (
<Badge key={skill} variant="light">
{skill}
</Badge>
))}
</Group>
</Box>
</Lightbox.Slide>
))}
</Lightbox.Slides>
</Lightbox.Root>
</>
);
}Videos
Embed iframes such as videos inside lightbox slides.



import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const youtubeVideos = [
{
id: "Tn6-PIqc4UM",
title: "React in 100 Seconds",
thumbnail: "https://img.youtube.com/vi/Tn6-PIqc4UM/hqdefault.jpg",
},
{
id: "OEV8gMkCHXQ",
title: "CSS in 100 Seconds",
thumbnail: "https://img.youtube.com/vi/OEV8gMkCHXQ/hqdefault.jpg",
},
{
id: "zQnBQ4tB3ZA",
title: "TypeScript in 100 Seconds",
thumbnail: "https://img.youtube.com/vi/zQnBQ4tB3ZA/hqdefault.jpg",
},
];
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 }}>
{youtubeVideos.map((video, index) => (
<Image
key={video.id}
src={video.thumbnail}
alt={video.title}
radius="md"
style={{ cursor: 'pointer' }}
onClick={() => open(index)}
/>
))}
</SimpleGrid>
<Lightbox.Root
opened={opened}
onClose={() => setOpened(false)}
initialSlide={initialSlide}
withZoom={false}
>
<Lightbox.Toolbar />
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides>
{youtubeVideos.map((video) => (
<Lightbox.Slide key={video.id}>
<div
style={{
width: '100%',
maxWidth: 900,
aspectRatio: '16 / 9',
}}
>
<iframe
src={`https://www.youtube.com/embed/${video.id}`}
title={video.title}
style={{
width: '100%',
height: '100%',
border: 'none',
borderRadius: 8,
}}
allowFullScreen
/>
</div>
<Lightbox.Caption>{video.title}</Lightbox.Caption>
</Lightbox.Slide>
))}
</Lightbox.Slides>
<Lightbox.Thumbnails>
{youtubeVideos.map((video) => (
<Lightbox.Thumbnail key={video.id}>
<img src={video.thumbnail} alt={video.title} />
</Lightbox.Thumbnail>
))}
</Lightbox.Thumbnails>
</Lightbox.Root>
</>
);
}Maps
Embed interactive maps using OpenStreetMap (or any other map iframe provider).
London
United Kingdom
Big Ben & Westminster
Paris
France
Eiffel Tower
New York
United States
Central Park
import { Card, SimpleGrid, Text, Title } from '@mantine/core';
import { Lightbox } from '@mantine-bites/lightbox';
import { useState } from 'react';
const locations = [
{
city: "London",
country: "United Kingdom",
landmark: "Big Ben & Westminster",
mapUrl:
"https://www.openstreetmap.org/export/embed.html?bbox=-0.13%2C51.49%2C-0.10%2C51.52",
},
{
city: "Paris",
country: "France",
landmark: "Eiffel Tower",
mapUrl:
"https://www.openstreetmap.org/export/embed.html?bbox=2.27%2C48.84%2C2.32%2C48.87",
},
{
city: "New York",
country: "United States",
landmark: "Central Park",
mapUrl:
"https://www.openstreetmap.org/export/embed.html?bbox=-74.01%2C40.70%2C-73.97%2C40.73",
},
];
function Demo() {
const [opened, setOpened] = useState(false);
const [initialSlide, setInitialSlide] = useState(0);
const open = (index) => {
setInitialSlide(index);
setOpened(true);
};
return (
<>
<SimpleGrid cols={{ base: 1, sm: 3 }}>
{locations.map((location, index) => (
<Card
key={location.city}
withBorder
radius="md"
style={{ cursor: 'pointer' }}
onClick={() => open(index)}
>
<Title order={4}>{location.city}</Title>
<Text size="sm" c="dimmed">
{location.country}
</Text>
<Text size="sm" mt="xs">
{location.landmark}
</Text>
</Card>
))}
</SimpleGrid>
<Lightbox.Root
opened={opened}
onClose={() => setOpened(false)}
initialSlide={initialSlide}
withZoom={false}
>
<Lightbox.Toolbar />
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides>
{locations.map((location) => (
<Lightbox.Slide key={location.city}>
<iframe
src={location.mapUrl}
title={location.city}
style={{ width: '100%', height: '100%', border: 'none' }}
/>
<Lightbox.Caption>
{location.city} — {location.landmark}
</Lightbox.Caption>
</Lightbox.Slide>
))}
</Lightbox.Slides>
</Lightbox.Root>
</>
);
}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",
caption: (
<>
A peaceful forest scene
<br />
<em>Photographed in the Pacific Northwest</em>
</>
),
},
{ src: "https://picsum.photos/id/20/1200/800", alt: "Books", caption: "A stack of 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)}
initialSlide={initialSlide}
>
<Lightbox.Toolbar />
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides>
{images.map((img) => (
<Lightbox.Slide key={img.src}>
<img src={img.src} alt={img.alt} />
{img.caption && <Lightbox.Caption>{img.caption}</Lightbox.Caption>}
</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)}
initialSlide={initialSlide}
>
<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>
{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)}
initialSlide={initialSlide}
>
<Lightbox.Toolbar />
<Lightbox.Counter />
<Lightbox.Controls />
<Lightbox.Slides>
{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>
</>
);
}