# Lightbox
Package: @thinker-core/mantine-lightbox
Import: import { Lightbox } from '@thinker-core/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 @thinker-core/mantine-lightbox
```

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

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

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

## Usage

`@thinker-core/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 '@thinker-core/mantine-lightbox/styles.css';
import { useState } from 'react';
import { Image, SimpleGrid } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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 '@thinker-core/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:

```tsx
import type { LightboxSlideData } from '@thinker-core/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):

```tsx
import { useState } from 'react';
import { Image, SimpleGrid } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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:

```tsx
import { useState } from 'react';
import { Image, SimpleGrid } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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%`:

```tsx
import type { LightboxSlideData } from '@thinker-core/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:

```tsx
import { useState } from 'react';
import { Image, SimpleGrid } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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 '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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:

```tsx
import { useState } from 'react';
import { Button } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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](https://www.embla-carousel.com/api/options/) to
the embla carousel. `loop` and `startIndex` options cannot be set this way, use `loop` and
`currentIndex` props instead:

```tsx
import { Lightbox } from '@thinker-core/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 '@thinker-core/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](https://mantine.blockfint.com/llms/core-transition.md) component. The overlay always fades. To change only
the duration (`200` by default), use `transitionDuration`:

```tsx
import { useState } from 'react';
import { Button, Group, Select } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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:

```tsx
import { useState } from 'react';
import { Button } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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 '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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:

```tsx
import { useState } from 'react';
import { Button, Group } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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:

```tsx
import { Lightbox } from '@thinker-core/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 '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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:

```tsx
import { Button } from '@thinker-core/mantine-core';
import { createLightbox, Lightbox } from '@thinker-core/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 '@thinker-core/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:

```tsx
import { useState } from 'react';
import { Button } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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`:

```tsx
import { useState } from 'react';
import { Button, Center, Text } from '@thinker-core/mantine-core';
import { Lightbox, LightboxSlideData } from '@thinker-core/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="ocean.10" 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="cherry.10" 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:

```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 '@thinker-core/mantine-core';
import {
  createCloseToolbarItem,
  createDownloadToolbarItem,
  createFullscreenToolbarItem,
  createThumbnailsToolbarItem,
  Lightbox,
  LightboxSlideData,
  ToolbarItem,
  ToolbarItemsPayload,
} from '@thinker-core/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:

```tsx
import { Lightbox } from '@thinker-core/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:

```tsx
import { Lightbox } from '@thinker-core/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.blockfint.com/llms/theming-default-props.md) 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:


#### 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. |
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |
| 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.Caption props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | React.ReactNode | - | Custom caption content, overrides slide caption |
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |

**Lightbox.CloseButton props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |

**Lightbox.Navigation props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |

**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. |
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |
| 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. |
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |
| 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.Slide props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| index | number | required | Index of the slide in the slides array |
| slide | LightboxSlideData | required | Slide data object |
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |

**Lightbox.Slides props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | React.ReactNode | required | Slide components |
| emblaRef | (instance: HTMLDivElement \| null) => void \| (() => VoidOrUndefinedOnly) | - | Embla carousel ref callback |
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |

**Lightbox.Thumbnails props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |

**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. |
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |


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