Lightbox

Full-screen media lightbox with carousel navigation

License

Installation

yarn add embla-carousel@^8.5.2 embla-carousel-react@^8.5.2 @mantine/lightbox

After installation import package styles at the root of your application:

import '@mantine/core/styles.css';
import '@mantine/lightbox/styles.css';

Usage

@mantine/lightbox is a full-screen media lightbox built on embla carousel. Click any image to open the lightbox:

import '@mantine/lightbox/styles.css';
import { useState } from 'react';
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const images = [
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png',
];

const slides: LightboxSlideData[] = images.map((src) => ({ src }));

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
      />

      <SimpleGrid cols={3}>
        {images.map((src, i) => (
          <Image
            key={src}
            src={src}
            radius="md"
            style={{ cursor: 'pointer' }}
            onClick={() => {
              setIndex(i);
              setOpened(true);
            }}
          />
        ))}
      </SimpleGrid>
    </>
  );
}

Slide data

The slides prop accepts an array of LightboxSlideData objects. There are three slide types – image (default), video and custom:

import type { LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  // Image slide (default type)
  {
    src: 'image.png', // Image URL
    alt: 'Mountain lake', // Alt text, always set it for screen readers
    caption: 'Mountain lake at sunrise', // Caption displayed below the slide
    thumbSrc: 'image-small.png', // Custom thumbnail URL, `src` is used if not set
    renderThumb: ({ active }) => <span>Thumb</span>, // Custom thumbnail content, see below
    srcSet: 'image-2x.png 2x', // Optional srcset for responsive images
    sizes: '(max-width: 600px) 100vw, 50vw', // Optional sizes attribute
    loading: 'lazy', // Optional loading attribute, see below
  },

  // Video slide
  {
    type: 'video',
    src: 'video.mp4',
    label: 'Product demo', // Accessible video label
    poster: 'poster.png', // Poster image, also used as thumbnail
    thumbSrc: 'poster-small.png', // Custom thumbnail URL, `poster` is used if not set
    renderThumb: ({ active }) => <span>Thumb</span>, // Custom thumbnail content, see below
    autoPlay: true, // Auto-play when the slide becomes active
    tracks: [{ src: 'captions.vtt', kind: 'captions', srcLang: 'en', label: 'English' }],
  },

  // Custom slide
  {
    type: 'custom',
    render: ({ active }) => <div>Custom content</div>,
    thumbSrc: 'thumb.png', // Custom thumbnail URL
    renderThumb: ({ active }) => <span>Thumb</span>, // Custom thumbnail content, see below
  },
];

Image loading

By default, only the image of the active slide is loaded when the lightbox is opened, other images and thumbnails are loaded lazily. Thumbnails use src if thumbSrc is not set, set thumbSrc to a smaller image if the originals are large.

Set the loading property on an image slide to change this, for example to preload the next slide:

import type { LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'image-1.png', alt: 'First' },
  { src: 'image-2.png', alt: 'Second', loading: 'eager' },
];

Zoom

Enable image zoom with the withZoom prop. On desktop, click an image to zoom in, scroll wheel to adjust zoom level, and drag or use arrow keys to pan when zoomed. On mobile, double-tap to zoom and pinch to adjust. Use zoomMaxScale to change the maximum zoom scale (3 by default):

Click image to zoom, scroll to adjust, drag to pan when zoomed. Press Z to toggle zoom via keyboard.

import { useState } from 'react';
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const images = [
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png',
];

const slides: LightboxSlideData[] = images.map((src) => ({ src }));

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
        withZoom
      />

      <SimpleGrid cols={3}>
        {images.map((src, i) => (
          <Image
            key={src}
            src={src}
            radius="md"
            style={{ cursor: 'pointer' }}
            onClick={() => {
              setIndex(i);
              setOpened(true);
            }}
          />
        ))}
      </SimpleGrid>
    </>
  );
}

Thumbnails

Set withThumbnails to display the thumbnail strip at the bottom. Users can hide it with the T key or the toolbar button:

import { useState } from 'react';
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const images = [
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png',
];

const slides: LightboxSlideData[] = images.map((src) => ({ src }));

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
        withThumbnails
      />

      <SimpleGrid cols={3}>
        {images.map((src, i) => (
          <Image
            key={src}
            src={src}
            radius="md"
            style={{ cursor: 'pointer' }}
            onClick={() => {
              setIndex(i);
              setOpened(true);
            }}
          />
        ))}
      </SimpleGrid>
    </>
  );
}

Custom thumbnails

By default, thumbnails are images. Set renderThumb on a slide to render custom thumbnail content instead, for example a <video> element. The content is placed inside a 64x64px button, size it with width: 100% and height: 100%:

import type { LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  {
    type: 'video',
    src: 'video.mp4',
    renderThumb: () => (
      <video
        src="video-thumb.mp4"
        muted
        playsInline
        preload="metadata"
        style={{ width: '100%', height: '100%', objectFit: 'cover' }}
      />
    ),
  },
];

All features

Zoom, thumbnails, fullscreen and download features can be combined:

import { useState } from 'react';
import { Image, SimpleGrid } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const images = [
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png',
];

const slides: LightboxSlideData[] = images.map((src) => ({ src }));

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
        withZoom
        withThumbnails
        withFullscreen
        withDownload
      />

      <SimpleGrid cols={3}>
        {images.map((src, i) => (
          <Image
            key={src}
            src={src}
            radius="md"
            style={{ cursor: 'pointer' }}
            onClick={() => {
              setIndex(i);
              setOpened(true);
            }}
          />
        ))}
      </SimpleGrid>
    </>
  );
}

Loop navigation

Set loop to enable infinite wrapping at the ends of the slide list:

import { useState } from 'react';
import { Button } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png' },
];

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
        loop
      />

      <Button onClick={() => setOpened(true)}>
        Open lightbox with loop
      </Button>
    </>
  );
}

Slide transition

By default, slides are changed instantly when arrow buttons or keyboard are used. Set withSlideTransition to animate slide changes. Use emblaOptions={{ duration: 40 }} to change the animation speed. Embla duration is not in milliseconds, values between 20 and 60 are recommended:

import { useState } from 'react';
import { Button } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png' },
];

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
        withSlideTransition
        withThumbnails
      />

      <Button onClick={() => setOpened(true)}>
        Open lightbox with slide transition
      </Button>
    </>
  );
}

Embla options

Use emblaOptions prop to pass options to the embla carousel. loop and startIndex options cannot be set this way, use loop and currentIndex props instead:

import { Lightbox } from '@mantine/lightbox';

function Demo() {
  return (
    <Lightbox
      opened
      onClose={() => {}}
      slides={[]}
      emblaOptions={{ dragFree: false, duration: 30, align: 'center' }}
    />
  );
}

z-index

zIndex controls the z-index of the overlay and the content elements, 400 by default:

import { Lightbox } from '@mantine/lightbox';

function Demo() {
  return <Lightbox opened onClose={() => {}} slides={[]} zIndex={1000} />;
}

Open and close transition

Use transitionProps to change the animation of the content, it accepts the same options as the Transition component. The overlay always fades. To change only the duration (200 by default), use transitionDuration:

import { useState } from 'react';
import { Button, Group, Select } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png' },
];

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);
  const [transition, setTransition] = useState<string | null>('pop');

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
        transitionProps={{ transition: transition as any, duration: 400 }}
      />

      <Group align="flex-end">
        <Select
          label="Transition"
          data={['fade', 'pop', 'scale', 'slide-up', 'slide-down', 'rotate-left']}
          value={transition}
          onChange={setTransition}
          allowDeselect={false}
        />
        <Button onClick={() => setOpened(true)}>Open lightbox</Button>
      </Group>
    </>
  );
}

Disable animations

Set transitionProps={{ duration: 0 }} to open and close the lightbox without animation:

import { useState } from 'react';
import { Button } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png' },
];

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
        transitionProps={{ duration: 0 }}
      />

      <Button onClick={() => setOpened(true)}>Open lightbox without animation</Button>
    </>
  );
}

Swipe to close

On mobile, swiping down closes the lightbox. This is enabled by default. Set closeOnSwipeDown={false} to disable:

import { useState } from 'react';
import { Button, Group } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png' },
];

function Demo() {
  const [opened, setOpened] = useState(false);
  const [swipeEnabled, setSwipeEnabled] = useState(true);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        closeOnSwipeDown={swipeEnabled}
      />

      <Group>
        <Button onClick={() => setOpened(true)}>
          Open lightbox
        </Button>

        <Button
          variant="default"
          onClick={() => setSwipeEnabled((v) => !v)}
        >
          Swipe close: {swipeEnabled ? 'enabled' : 'disabled'}
        </Button>
      </Group>
    </>
  );
}

Click outside to close

Set closeOnClickOutside to close the lightbox when the empty space around the slide content is clicked. It is disabled by default to prevent accidental closing:

import { useState } from 'react';
import { Button, Group } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png' },
];

function Demo() {
  const [opened, setOpened] = useState(false);
  const [closeOnClickOutside, setCloseOnClickOutside] = useState(true);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        closeOnClickOutside={closeOnClickOutside}
      />

      <Group>
        <Button onClick={() => setOpened(true)}>
          Open lightbox
        </Button>

        <Button
          variant="default"
          onClick={() => setCloseOnClickOutside((v) => !v)}
        >
          Close on click outside: {closeOnClickOutside ? 'enabled' : 'disabled'}
        </Button>
      </Group>
    </>
  );
}

Store API

Mount Lightbox.Provider once in your app and open the lightbox from anywhere using the static Lightbox methods:

import { Lightbox } from '@mantine/lightbox';

// Open with slides and optional start index
Lightbox.open({ slides, startIndex: 2 });

// Close
Lightbox.close();

// Navigate
Lightbox.next();
Lightbox.prev();
Lightbox.setIndex(5);
import { Button, Group } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png', caption: 'Slide 1' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png', caption: 'Slide 2' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png', caption: 'Slide 3' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png', caption: 'Slide 4' },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-5.png', caption: 'Slide 5' },
];

function Demo() {
  return (
    <>
      <Lightbox.Provider withThumbnails />

      <Group>
        <Button onClick={() => Lightbox.open({ slides })}>
          Open lightbox
        </Button>

        <Button
          variant="default"
          onClick={() => Lightbox.open({ slides, startIndex: 2 })}
        >
          Open at slide 3
        </Button>
      </Group>
    </>
  );
}

Multiple lightboxes

By default, Lightbox.Provider and the static Lightbox methods use the shared lightboxStore. To use several independent lightboxes, create a separate store with createLightbox and pass it to the store prop:

import { Button } from '@mantine/core';
import { createLightbox, Lightbox } from '@mantine/lightbox';

const [productStore, productLightbox] = createLightbox();

function Demo() {
  return (
    <>
      <Lightbox.Provider store={productStore} withThumbnails />
      <Button onClick={() => productLightbox.open({ slides })}>Open product gallery</Button>
    </>
  );
}

To subscribe to the state of any lightbox store in a component, use the useLightboxStore hook:

import { lightboxStore, useLightboxStore } from '@mantine/lightbox';

function Demo() {
  const { opened, currentIndex, slides } = useLightboxStore(lightboxStore);
  return <div>Current slide: {currentIndex + 1}</div>;
}

Video slides

Set type: 'video' on a slide to render a <video> element. Videos are paused when the slide is changed. The poster image is used as the thumbnail unless thumbSrc or renderThumb is set:

Opens on a video slide with autoPlay. Navigate away to see the video pause automatically.

import { useState } from 'react';
import { Button } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  {
    type: 'video',
    src: 'https://www.w3schools.com/html/mov_bbb.mp4',
    poster: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
    caption: 'Play this video, then navigate to the next slide – it pauses automatically',
    autoPlay: true,
  },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png', caption: 'Image slide' },
  {
    type: 'video',
    src: 'https://www.w3schools.com/html/mov_bbb.mp4',
    poster: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-4.png',
    caption: 'Another video',
  },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png', caption: 'Another image' },
  {
    type: 'video',
    src: 'https://www.w3schools.com/html/mov_bbb.mp4',
    caption: 'Video with a video thumbnail rendered with renderThumb',
    renderThumb: () => (
      <video
        src="https://www.w3schools.com/html/mov_bbb.mp4"
        muted
        playsInline
        preload="metadata"
        style={{ width: '100%', height: '100%', objectFit: 'cover' }}
      />
    ),
  },
];

function Demo() {
  const [opened, setOpened] = useState(false);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        withThumbnails
      />

      <Button onClick={() => setOpened(true)}>
        Open lightbox with videos
      </Button>
    </>
  );
}

Custom slides

Set type: 'custom' and a render function to display any content in a slide. Custom slides do not have a default thumbnail, set renderThumb or thumbSrc:

import { useState } from 'react';
import { Button, Center, Text } from '@mantine/core';
import { Lightbox, LightboxSlideData } from '@mantine/lightbox';

const slides: LightboxSlideData[] = [
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png' },
  {
    type: 'custom',
    render: ({ active }) => (
      <Center h="100%">
        <Text c="white" size="xl" fw={700}>
          {active ? 'This slide is active' : 'This slide is not active'}
        </Text>
      </Center>
    ),
    renderThumb: () => (
      <Center h="100%" bg="blue.6" style={{ borderRadius: 4 }}>
        <Text c="white" size="xs">Custom</Text>
      </Center>
    ),
    caption: 'Custom slide with render function',
  },
  {
    type: 'custom',
    render: () => (
      <iframe
        src="https://www.youtube.com/embed/dQw4w9WgXcQ"
        title="YouTube video"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
        style={{ width: '80vw', height: '45vw', maxHeight: '70vh', border: 'none', borderRadius: 8 }}
      />
    ),
    renderThumb: () => (
      <Center h="100%" bg="red.6" style={{ borderRadius: 4 }}>
        <Text c="white" size="xs">YT</Text>
      </Center>
    ),
    caption: 'Embedded YouTube video',
  },
  { src: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png' },
];

function Demo() {
  const [opened, setOpened] = useState(false);
  const [index, setIndex] = useState(0);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        currentIndex={index}
        onIndexChange={setIndex}
        withThumbnails
      />

      <Button onClick={() => setOpened(true)}>
        Open lightbox with custom slides
      </Button>
    </>
  );
}

Custom toolbar

Use toolbarItems prop to replace the default toolbar items. It accepts an array of items or a function that returns it. The function is called with the lightbox state and handlers:

interface ToolbarItemsPayload {
  slides: LightboxSlideData[];
  currentIndex: number;
  setIndex: (index: number) => void;
  next: () => void;
  prev: () => void;
  close: () => void;
  thumbnailsVisible: boolean;
  toggleThumbnails: () => void;
  isFullscreen: boolean;
  toggleFullscreen: () => void;
  zoomed: boolean;
  toggleZoom: () => void;
}

Use the built-in toolbar item factories for common actions:

import { useState } from 'react';
import { Button } from '@mantine/core';
import {
  createCloseToolbarItem,
  createDownloadToolbarItem,
  createFullscreenToolbarItem,
  createThumbnailsToolbarItem,
  Lightbox,
  LightboxSlideData,
  ToolbarItem,
  ToolbarItemsPayload,
} from '@mantine/lightbox';

const images = [
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
  'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
];

const slides: LightboxSlideData[] = images.map((src) => ({ src }));

function InfoIcon() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
      <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" />
    </svg>
  );
}

// toolbarItems as a function receives the current lightbox state and handlers
const toolbarItems = (payload: ToolbarItemsPayload): ToolbarItem[] => [
  // Built-in factories for common actions
  createThumbnailsToolbarItem(payload.toggleThumbnails, payload.thumbnailsVisible, payload.labels),
  createFullscreenToolbarItem(payload.toggleFullscreen, payload.isFullscreen, payload.labels),
  createDownloadToolbarItem(images[payload.currentIndex], payload.labels),
  // Fully custom toolbar item
  {
    key: 'info',
    icon: <InfoIcon />,
    label: 'Image info',
    position: 'right',
    onClick: () => {
      // eslint-disable-next-line no-alert
      alert(`Viewing image ${payload.currentIndex + 1} of ${payload.slides.length}`);
    },
  },
  createCloseToolbarItem(payload.close, payload.labels),
];

function Demo() {
  const [opened, setOpened] = useState(false);

  return (
    <>
      <Lightbox
        opened={opened}
        onClose={() => setOpened(false)}
        slides={slides}
        withThumbnails
        toolbarItems={toolbarItems}
      />

      <Button onClick={() => setOpened(true)}>
        Open lightbox with custom toolbar
      </Button>
    </>
  );
}

Compound components

To change the layout of the lightbox, use compound components:

import { Lightbox } from '@mantine/lightbox';

function CustomLightbox({ opened, onClose, slides }) {
  return (
    <Lightbox.Root opened={opened} onClose={onClose} slides={slides} withZoom withThumbnails>
      <Lightbox.Toolbar />
      <Lightbox.Slides>
        {slides.map((slide, index) => (
          <Lightbox.Slide key={index} slide={slide} index={index} />
        ))}
      </Lightbox.Slides>
      <Lightbox.Navigation />
      <Lightbox.Caption />
      <Lightbox.Thumbnails />
    </Lightbox.Root>
  );
}

Lightbox.Thumbnails is rendered only when withThumbnails is set on Lightbox.Root.

Labels

Use labels prop to translate the lightbox. Labels that are not set fall back to the default English values:

import { Lightbox } from '@mantine/lightbox';

function Demo() {
  return (
    <Lightbox
      opened
      onClose={() => {}}
      slides={[]}
      labels={{
        lightboxLabel: 'Galerie',
        slideLabel: (index, total) => `Bild ${index} von ${total}`,
        slidesLabel: 'Bilder',
        thumbnailLabel: (index) => `Zu Bild ${index} wechseln`,
        previousSlideLabel: 'Vorheriges Bild',
        nextSlideLabel: 'Nächstes Bild',
        enterFullscreenLabel: 'Vollbild aktivieren',
        exitFullscreenLabel: 'Vollbild beenden',
        showThumbnailsLabel: 'Miniaturansichten anzeigen',
        hideThumbnailsLabel: 'Miniaturansichten ausblenden',
        downloadLabel: 'Herunterladen',
        closeLabel: 'Schließen',
      }}
    />
  );
}

To change labels for all lightboxes in your application, set labels in default props of the Lightbox component.

Accessibility

  • Always set the alt property on image slides and the label property on video slides and custom toolbar items
  • The lightbox has role="dialog" and is labelled with labels.lightboxLabel, set aria-label to change the label of a single lightbox
  • Focus is trapped inside the lightbox and is returned to the previously focused element when it is closed
  • Slide changes are announced with aria-live="polite"
  • Inactive slides are hidden from screen readers and removed from the tab order

Keyboard shortcuts

Set withKeyboardEvents={false} to disable all shortcuts except Escape. Shortcuts are ignored while focus is inside an input, a video or an interactive element of a custom slide:

KeyDescription
EscapeClose lightbox
ArrowLeftPrevious slide, pan left when zoomed
ArrowRightNext slide, pan right when zoomed
ArrowUpPan up when zoomed
ArrowDownPan down when zoomed
FToggle fullscreen (requires withFullscreen)
TToggle thumbnails (requires withThumbnails)
ZToggle zoom (requires withZoom)