# Lightbox
Package: @mantine/lightbox
Import: import { Lightbox } from '@mantine/lightbox';
Description: Full-screen media lightbox with carousel navigation

## Installation

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

```bash
npm install embla-carousel@^8.5.2 embla-carousel-react@^8.5.2 @mantine/lightbox
```

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

```tsx
import '@mantine/core/styles.css';
// ‼️ import lightbox styles after core package styles
import '@mantine/lightbox/styles.css';
```

## Usage

`@mantine/lightbox` is a full-screen media lightbox built on [embla carousel](https://www.embla-carousel.com/).
Click any image to open the lightbox:

```tsx
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:

```tsx
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
    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
    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>,
    renderThumb: () => <span>Thumb</span>, // Custom thumbnail content
  },
];
```

### Image loading

All slides are mounted at once, so images are loaded lazily by default – only the active slide uses
`loading="eager"`, every other slide uses `loading="lazy"` and is downloaded by the browser as it
comes into view. This way opening a gallery with a large number of slides does not start a download
for every image at once. Thumbnails are always lazy – note that `thumbSrc` falls back to `src`, so
set it to a smaller image if the originals are large.

Set the `loading` property on an image slide to override this per slide, for example to eagerly
preload the slide next to the one that is opened first:

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

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

## Navigation and close behavior

Use the following props to control built-in interactions:

* `withNavigation` – shows previous/next arrow buttons, `true` by default
* `loop` – enables infinite loop navigation, `false` by default
* `closeOnClickOutside` – closes the lightbox when the empty space around the slide content is clicked, `false` by default
* `closeOnSwipeDown` – closes the lightbox on mobile swipe down, `true` by default
* `withKeyboardEvents` – enables keyboard shortcuts (arrows, `F`/`T`/`Z`), `true` by default; `Escape` always closes the lightbox
* `returnFocus` – returns focus to the last active element when the lightbox is closed, `true` by default
* `withInitialFocusPlaceholder` – adds a hidden focusable element at the start of the lightbox content so that the first toolbar button does not receive visible focus when the lightbox is opened with a pointer, `true` by default

## 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):

```tsx
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

Enable the bottom thumbnail strip with `withThumbnails`. Click a thumbnail to navigate to that slide.
Toggle visibility at runtime with the `T` keyboard shortcut or the toolbar button – the strip is
animated with the `transitionDuration` prop value:

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


## All features

Combine `withZoom`, `withThumbnails`, `withFullscreen`, and `withDownload` for the full experience:

```tsx
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:

```tsx
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, programmatic navigation (arrow buttons, keyboard, controlled index changes) snaps
instantly. Set `withSlideTransition` to animate slide changes. The animation is handled by embla –
use `emblaOptions={{ duration: 40 }}` to change its speed (embla duration is not in milliseconds,
values between 20 and 60 are recommended):

```tsx
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

The carousel behavior can be customized with the `emblaOptions` prop – it is passed directly to the
underlying [embla carousel](https://www.embla-carousel.com/api/options/) instance. For example, you
can change drag behavior or scroll animation speed.

`loop` and `startIndex` are managed by the `loop` and `currentIndex` props and cannot be set through
`emblaOptions`. A `watchDrag` callback is still called, but dragging is always disabled while the
image is zoomed, so the zoom gesture is not interrupted:

```tsx
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:

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

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

## Open and close transition

By default, the overlay fades in and the content pops in – it is scaled from 95% to 100% while
fading. Use `transitionProps` to change the animation of the content – the overlay always fades.
`transitionProps` accepts the same options as the [Transition](https://mantine.dev/llms/core-transition.md) component
(`transition`, `duration`, `timingFunction`). Use `transitionDuration` as a shorthand to change only
the duration (`200` by default):

```tsx
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 instantly, without any
animation:

```tsx
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:

```tsx
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 current slide
content is clicked – clicks on the image, video or any custom slide content are ignored, as well as
clicks on the toolbar, navigation buttons, caption and thumbnails. The option is disabled by default
to prevent accidental closing:

```tsx
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 (aliases of the default `lightbox` store actions):

```tsx
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);
```

```tsx
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.open/close/next/prev/setIndex` methods
use the shared `lightboxStore`. To run several independent lightboxes, create an isolated store
with actions bound to it using `createLightbox` and pass the store to the `store` prop:

```tsx
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:

```tsx
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 automatically
paused when navigating away from the slide:

```tsx
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' },
];

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'` with a `render` function for fully custom slide content.
Use `renderThumb` to customize the thumbnail as well:

```tsx
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

Override the default toolbar items with the `toolbarItems` prop. Each item has
a `key`, `icon`, `label`, `onClick`, and optional `position` (`'left'` or `'right'`).

`toolbarItems` can be either an array or a function that receives the current lightbox state and
handlers – use the function form to build items that depend on the internal state, for example
the thumbnails, fullscreen and zoom toggles. The function receives the following payload:

```tsx
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:

```tsx
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

For full layout control, compose sub-components directly:

```tsx
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>
  );
}
```

Note that `Lightbox.Thumbnails` renders only when `withThumbnails` is set on `Lightbox.Root`.

Available sub-components:

* `Lightbox.Root` — overlay, portal, focus trap, scroll lock, keyboard handling
* `Lightbox.Toolbar` — top bar with actions and slide counter
* `Lightbox.Slides` — Embla carousel wrapper
* `Lightbox.Slide` — individual slide (image, video, or custom)
* `Lightbox.Thumbnails` — bottom thumbnail strip
* `Lightbox.Navigation` — prev/next arrow buttons
* `Lightbox.Caption` — text below the active slide
* `Lightbox.CloseButton` — standalone close button
* `Lightbox.Provider` — store-mode mount point

## Labels

All strings rendered by the lightbox are defined in the `labels` prop. Pass the labels
that you want to change – the rest fall back to the default English values, which are
exported as `DEFAULT_LABELS`:

```tsx
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](https://mantine.dev/llms/theming-default-props.md) of the `Lightbox` component.

## Accessibility

* Focus is trapped within the lightbox when opened and returned to the last active element when it is closed (`returnFocus` prop)
* Focus is moved to a hidden placeholder element instead of the first toolbar button when the lightbox is opened, so that no focus ring is displayed for pointer users (`withInitialFocusPlaceholder` prop)
* The content element has `role="dialog"` with `aria-modal="true"` and an accessible name from `labels.lightboxLabel` – pass `aria-label` to override it for a single lightbox
* Always set the `alt` property on image slides – without it the image is treated as decorative by screen readers; for video slides set the `label` property
* All interactive elements have `aria-label` attributes; the `label` property is required for custom toolbar items
* `aria-live="polite"` region announces slide changes (including the current slide `alt`/`label`) to screen readers
* Inactive slides are `inert` – their content is hidden from screen readers and removed from the tab order, so only the current slide is reachable
* Body scroll is locked when the lightbox is open
* Fullscreen mode entered from the toolbar is exited automatically when the lightbox is closed

## Keyboard shortcuts

`Escape` always closes the lightbox. Other shortcuts are active when `withKeyboardEvents` is set
(default). They are ignored when focus is inside an input, textarea or media element anywhere in the
lightbox, and when focus is on a button, link or other widget inside slide content – so controls in
custom slides keep their own keyboard handling. `F`/`T`/`Z` only work when the corresponding feature is enabled:


#### Props

**Lightbox props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | React.ReactNode | - | Custom lightbox layout, overrides the default layout built from compound components |
| closeOnClickOutside | boolean | - | Closes lightbox when the empty space around the slide content is clicked |
| closeOnSwipeDown | boolean | - | Closes lightbox when swiping down on mobile |
| currentIndex | number | - | Controlled current slide index |
| emblaOptions | Partial<OptionsType> | - | Additional Embla carousel options. `loop` and `startIndex` are controlled by the `loop` and `currentIndex` props and cannot be set here, at the top level or in `breakpoints`; a `watchDrag` option is still honored at both levels, but dragging is always disabled while the image is zoomed. |
| labels | Partial<LightboxLabels> | - | Labels used in the component, used for accessibility and localization |
| loop | boolean | - | Enables infinite loop navigation |
| onClose | () => void | required | Called when the lightbox is closed |
| onIndexChange | (index: number) => void | - | Called when the current slide index changes |
| opened | boolean | required | Controls whether the lightbox is opened |
| returnFocus | boolean | - | Determines whether focus should be returned to the last active element when the lightbox is closed |
| slides | LightboxSlideData[] | required | Array of slide data objects |
| toolbarItems | ToolbarItems | - | Custom toolbar items, overrides default toolbar. Can be a function that receives the current lightbox state and handlers. |
| transitionDuration | number | - | Transition duration in milliseconds |
| transitionProps | TransitionProps | - | Props passed down to the `Transition` component that animates the content, the overlay always fades. By default, the content is scaled from 95% to 100% while fading in. |
| withDownload | boolean | - | Adds download button to toolbar |
| withFullscreen | boolean | - | Adds fullscreen toggle to toolbar |
| withInitialFocusPlaceholder | boolean | - | Adds a hidden focusable element at the start of the lightbox content – prevents the first toolbar button from receiving visible focus when the lightbox is opened with a pointer. Set to `false` if you need custom focus management. |
| withKeyboardEvents | boolean | - | Determines whether keyboard shortcuts (arrows, `F`/`T`/`Z`) are active, `Escape` always closes the lightbox |
| withNavigation | boolean | - | Shows previous/next navigation arrows |
| withSlideTransition | boolean | - | Enables animated slide transitions for programmatic navigation |
| withThumbnails | boolean | - | Shows bottom thumbnail strip |
| withZoom | boolean | - | Enables image zoom on click/pinch |
| withinPortal | boolean | - | Determines whether the lightbox should be rendered inside `Portal` |
| zIndex | string \| number | - | `z-index` of the overlay and content elements, `400` by default |
| zoomMaxScale | number | - | Maximum zoom scale |

**Lightbox.Root props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | React.ReactNode | required | Lightbox content (compound components) |
| closeOnClickOutside | boolean | - | Closes lightbox when the empty space around the slide content is clicked |
| closeOnSwipeDown | boolean | - | Closes lightbox when swiping down on mobile |
| currentIndex | number | - | Controlled current slide index |
| emblaOptions | Partial<OptionsType> | - | Additional Embla carousel options. `loop` and `startIndex` are controlled by the `loop` and `currentIndex` props and cannot be set here, at the top level or in `breakpoints`; a `watchDrag` option is still honored at both levels, but dragging is always disabled while the image is zoomed. |
| labels | Partial<LightboxLabels> | - | Labels used in the component, used for accessibility and localization |
| loop | boolean | - | Enables infinite loop navigation |
| onClose | () => void | required | Called when the lightbox is closed |
| onIndexChange | (index: number) => void | - | Called when the current slide index changes |
| opened | boolean | required | Controls whether the lightbox is opened |
| returnFocus | boolean | - | Determines whether focus should be returned to the last active element when the lightbox is closed |
| slides | LightboxSlideData[] | required | Array of slide data objects |
| transitionDuration | number | - | Transition duration in milliseconds |
| transitionProps | TransitionProps | - | Props passed down to the `Transition` component that animates the content, the overlay always fades. By default, the content is scaled from 95% to 100% while fading in. |
| withDownload | boolean | - | Adds download button to toolbar |
| withFullscreen | boolean | - | Adds fullscreen toggle to toolbar |
| withInitialFocusPlaceholder | boolean | - | Adds a hidden focusable element at the start of the lightbox content – prevents the first toolbar button from receiving visible focus when the lightbox is opened with a pointer. Set to `false` if you need custom focus management. |
| withKeyboardEvents | boolean | - | Determines whether keyboard shortcuts (arrows, `F`/`T`/`Z`) are active, `Escape` always closes the lightbox |
| withSlideTransition | boolean | - | Enables animated slide transitions for programmatic navigation |
| withThumbnails | boolean | - | Shows bottom thumbnail strip |
| withZoom | boolean | - | Enables image zoom on click/pinch |
| withinPortal | boolean | - | Determines whether the lightbox should be rendered inside `Portal` |
| zIndex | string \| number | - | `z-index` of the overlay and content elements, `400` by default |
| zoomMaxScale | number | - | Maximum zoom scale |

**Lightbox.Toolbar props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| toolbarItems | ToolbarItems | - | Custom toolbar items, overrides default toolbar actions. Can be a function that receives the current lightbox state and handlers. |

**Lightbox.Slides props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | React.ReactNode | required | Slide components |
| emblaRef | (instance: HTMLDivElement \| null) => void \| (() => VoidOrUndefinedOnly) | - | Embla carousel ref callback |

**Lightbox.Slide props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| index | number | required | Index of the slide in the slides array |
| slide | LightboxSlideData | required | Slide data object |

**Lightbox.Thumbnails props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|

**Lightbox.Navigation props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|

**Lightbox.Caption props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | React.ReactNode | - | Custom caption content, overrides slide caption |

**Lightbox.CloseButton props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|

**Lightbox.Provider props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | React.ReactNode | - | Custom lightbox layout, overrides the default layout built from compound components |
| closeOnClickOutside | boolean | - | Closes lightbox when the empty space around the slide content is clicked |
| closeOnSwipeDown | boolean | - | Closes lightbox when swiping down on mobile |
| emblaOptions | Partial<OptionsType> | - | Additional Embla carousel options. `loop` and `startIndex` are controlled by the `loop` and `currentIndex` props and cannot be set here, at the top level or in `breakpoints`; a `watchDrag` option is still honored at both levels, but dragging is always disabled while the image is zoomed. |
| labels | Partial<LightboxLabels> | - | Labels used in the component, used for accessibility and localization |
| loop | boolean | - | Enables infinite loop navigation |
| returnFocus | boolean | - | Determines whether focus should be returned to the last active element when the lightbox is closed |
| store | LightboxStore | - | Lightbox store, can be used to create multiple instances |
| toolbarItems | ToolbarItems | - | Custom toolbar items, overrides default toolbar. Can be a function that receives the current lightbox state and handlers. |
| transitionDuration | number | - | Transition duration in milliseconds |
| transitionProps | TransitionProps | - | Props passed down to the `Transition` component that animates the content, the overlay always fades. By default, the content is scaled from 95% to 100% while fading in. |
| withDownload | boolean | - | Adds download button to toolbar |
| withFullscreen | boolean | - | Adds fullscreen toggle to toolbar |
| withInitialFocusPlaceholder | boolean | - | Adds a hidden focusable element at the start of the lightbox content – prevents the first toolbar button from receiving visible focus when the lightbox is opened with a pointer. Set to `false` if you need custom focus management. |
| withKeyboardEvents | boolean | - | Determines whether keyboard shortcuts (arrows, `F`/`T`/`Z`) are active, `Escape` always closes the lightbox |
| withNavigation | boolean | - | Shows previous/next navigation arrows |
| withSlideTransition | boolean | - | Enables animated slide transitions for programmatic navigation |
| withThumbnails | boolean | - | Shows bottom thumbnail strip |
| withZoom | boolean | - | Enables image zoom on click/pinch |
| withinPortal | boolean | - | Determines whether the lightbox should be rendered inside `Portal` |
| zIndex | string \| number | - | `z-index` of the overlay and content elements, `400` by default |
| zoomMaxScale | number | - | Maximum zoom scale |


#### Styles API

Lightbox component supports Styles API. With Styles API, you can customize styles of any inner element. Follow the documentation to learn how to use CSS modules, CSS variables and inline styles to get full control over component styles.

**Lightbox selectors**

| Selector | Static selector | Description |
|----------|----------------|-------------|
| root | .mantine-Lightbox-root | Root element – holds CSS variables, renders overlay and content |
| overlay | .mantine-Lightbox-overlay | Background overlay behind the lightbox |
| content | .mantine-Lightbox-content | Full-screen dialog container with all lightbox controls |
| toolbar | .mantine-Lightbox-toolbar | Top toolbar wrapper |
| toolbarGroup | .mantine-Lightbox-toolbarGroup | Left/right toolbar button group |
| toolbarButton | .mantine-Lightbox-toolbarButton | Individual toolbar action button |
| counter | .mantine-Lightbox-counter | Slide counter text (e.g. "2 / 5") |
| slides | .mantine-Lightbox-slides | Slides area wrapper |
| slidesViewport | .mantine-Lightbox-slidesViewport | Embla viewport (overflow hidden) |
| slidesContainer | .mantine-Lightbox-slidesContainer | Embla container (flex row of slides) |
| slide | .mantine-Lightbox-slide | Individual slide wrapper |
| slideImage | .mantine-Lightbox-slideImage | Image element inside a slide |
| slideVideo | .mantine-Lightbox-slideVideo | Video element inside a slide |
| thumbnails | .mantine-Lightbox-thumbnails | Thumbnails strip wrapper |
| thumbnailsViewport | .mantine-Lightbox-thumbnailsViewport | Thumbnails Embla viewport |
| thumbnailsContainer | .mantine-Lightbox-thumbnailsContainer | Thumbnails Embla container |
| thumbnail | .mantine-Lightbox-thumbnail | Individual thumbnail button |
| thumbnailImage | .mantine-Lightbox-thumbnailImage | Image inside a thumbnail |
| navigation | .mantine-Lightbox-navigation | Navigation arrows wrapper |
| navigationButton | .mantine-Lightbox-navigationButton | Previous/next navigation button |
| caption | .mantine-Lightbox-caption | Caption text below the active slide |
| closeButton | .mantine-Lightbox-closeButton | Close button |

**Lightbox CSS variables**

| Selector | Variable | Description |
|----------|----------|-------------|
| root | --lightbox-transition-duration | Controls transition duration of all animations |
| root | --lightbox-overlay-color | Controls background overlay color |
| root | --lightbox-z-index | Controls `z-index` of the overlay and content elements |
| root | --lightbox-toolbar-height | Controls toolbar height |
| root | --lightbox-thumbnails-height | Controls thumbnails strip height |

**Lightbox data attributes**

| Selector | Attribute | Condition | Value |
|----------|-----------|-----------|-------|
| thumbnail | data-active | Thumbnail corresponds to the currently active slide | - |
| navigationButton | data-inactive | Navigation button is disabled (first/last slide without loop) | - |
| slideImage | data-zoom-enabled | `withZoom` prop is set on the lightbox | - |
| slideImage | data-zoomed | Image is currently zoomed in | - |
| slideImage | data-dragging | Image is being dragged while zoomed | - |
