@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/lightbox

After 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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

Forest
Books
Mug
Cat
Bird
Computer
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.

React in 100 SecondsCSS in 100 SecondsTypeScript in 100 Seconds
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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.

ForestBooksMugCatBirdComputer
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>
    </>
  );
}