@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)}
        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.

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)}
        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.

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)}
        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.

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)}
        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.

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)}
        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.

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)}
        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.

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)}
        slidesProps={{ initialSlide, emblaPlugins: [autoplay] }}
      />
    </>
  );
}

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)}
        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.

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)}
        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.

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)}
        slidesProps={{ initialSlide }}
      />
    </>
  );
}

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" },
  { 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.

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)}>
        <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.

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