# Lightbox

> A family of composable components for overlay image galleries and custom HTML slides, powered by PhotoSwipe with built-in keyboard navigation, ARIA support, and lazy-loaded image handling.

# Lightbox

## Overview

Use the Lightbox family when you need an overlay gallery for images or custom HTML content. The root component manages gallery-wide behavior — navigation, animation, and presentation — while each Lightbox Item defines a clickable trigger and the full-size content shown when the overlay opens. Lightbox works well for image galleries, product photo viewers, portfolio showcases, and any content that benefits from a zoomable, swipeable full-screen presentation.

The component uses [PhotoSwipe](https://photoswipe.com/) under the hood and automatically detects image mode versus HTML mode based on what you place in each item's slots. Items within the same root form a gallery — arrow keys navigate between them.

## Authoring Structure

```
Lightbox
└── Lightbox Item
    ├── trigger slot
    │   └── preview content (what users click)
    └── full slot
        └── opened content (what the overlay shows)
```

### Placement Rules

| Component | Placement | Role |
|---|---|---|
| **Lightbox** | Top-level gallery wrapper. | Owns gallery state, runtime configuration, and PhotoSwipe initialization. |
| **Lightbox Item** | Direct child of `Lightbox`. | Each item becomes one gallery entry. Items are matched by position within the root. |
| **trigger** slot | Inside `Lightbox Item`. | The preview content users click to open the overlay. Can be an image, text, or any markup. |
| **full** slot | Inside `Lightbox Item`. | The content rendered when the lightbox opens. Can be an image, HTML, or any markup. |

## Quick Start

<CodeExample title="Basic image gallery" language="jsx">
{`<OmeLightbox>
  {#slot default}
    <OmeLightboxItem content='{{"triggerLabel":"Open image one"}}'>
      {#slot trigger}
        
      {/slot}
      {#slot full}
        
      {/slot}
    </OmeLightboxItem>
  {/slot}
</OmeLightbox>`}
</CodeExample>

When both the trigger and full slots contain exactly one `` each, the runtime automatically enters **image mode** — PhotoSwipe handles pinch-to-zoom, swipe gestures, and responsive image sizing natively.

<CodeExample title="Custom gallery behavior with HTML slides" language="jsx">
{`<OmeLightbox
  behavior='{{"loop":"{false}","wheelToZoom":"{true}"}}'
  animation='{{"showHideAnimationType":"fade","animationDuration":"300"}}'
>
  {#slot default}
    <OmeLightboxItem content='{{"triggerLabel":"Open HTML slide"}}'>
      {#slot trigger}View Details{/slot}
      {#slot full}
        
          Product Details
          Full product description rendered inside the lightbox overlay.
        
      {/slot}
    </OmeLightboxItem>
  {/slot}
</OmeLightbox>`}
</CodeExample>

When the full slot contains anything other than a single image, the runtime enters **HTML mode** — the content is rendered inside a centered, padded container within the overlay.

---

## Family Components

### Lightbox (Root)

The root component wraps all items and manages shared behavior: gallery navigation, animation settings, overlay appearance, and gesture handling. It does not render visible UI itself — only a container element with data attributes that the runtime reads on initialization.

<PropsTable group="Behavior">
  <Prop name="loop" type="boolean" defaultValue="true">
    Allows gallery navigation to wrap from last to first and first to last. When `false`, navigation stops at the first and last items.
  </Prop>
  <Prop name="closeOnVerticalDrag" type="boolean" defaultValue="true">
    Allows vertical drag gestures (swipe up or down) to close the lightbox overlay where supported.
  </Prop>
  <Prop name="wheelToZoom" type="boolean" defaultValue="false">
    Enables zooming with the mouse scroll wheel when the lightbox is open.
  </Prop>
</PropsTable>

<PropsTable group="Animation">
  <Prop name="showHideAnimationType" type={`"none" | "fade" | "zoom"`} defaultValue="none">
    Opening and closing animation type. `"none"` opens instantly, `"fade"` uses an opacity transition, and `"zoom"` scales from the trigger element.
  </Prop>
  <Prop name="animationDuration" type="string" defaultValue="250">
    Show and hide animation duration in milliseconds. Only applies when `showHideAnimationType` is not `"none"`.
  </Prop>
</PropsTable>

<PropsTable group="Presentation">
  <Prop name="bgOpacity" type="string" defaultValue="0.8">
    Background overlay opacity (0–1) used by the opened lightbox. Clamped to the 0–1 range at runtime.
  </Prop>
  <Prop name="spacing" type="string" defaultValue="0.12">
    Gallery item spacing as a ratio, forwarded to PhotoSwipe. Controls the gap between slides during swipe navigation.
  </Prop>
  <Prop name="htmlViewportPadding" type="string" defaultValue="24">
    Viewport padding in pixels applied around HTML-mode slides. Also sets the `--ome-lightbox-html-padding` CSS custom property.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-lightbox-default">
    CSS class applied to the root gallery wrapper element.
  </Prop>
</PropsTable>

#### Item Detection and Mode

The runtime automatically detects what mode to use for each Lightbox Item:

- **Image mode** — When both the trigger slot and the full slot each contain exactly one `` element, and the full-slot image has explicit `width` and `height` attributes. PhotoSwipe renders the image natively with pinch-to-zoom and responsive sizing. The trigger image is automatically used as the placeholder thumbnail (`msrc`) during the open animation.

- **HTML mode** — When the full slot contains anything other than a single image (or when the image is missing `width`/`height` attributes). The full slot's inner HTML is wrapped in a centered, scrollable container inside the overlay.

- **Lazy-loaded images** — The runtime resolves lazy-loaded images by checking `data-src`, `data-lazy-src`, `data-srcset`, and `data-lazy-srcset` attributes as fallbacks. `<picture>` element source sets are also resolved automatically.

#### Keyboard Behavior

When the lightbox overlay is open:

| Key | Action |
|---|---|
| `ArrowLeft` | Navigate to the previous gallery item. |
| `ArrowRight` | Navigate to the next gallery item. |
| `Escape` | Close the lightbox overlay and return focus to the trigger. |

When `loop` is `true`, navigating past the last item wraps to the first, and vice versa. Arrow keys, Escape, focus trapping, and focus return are always enabled and cannot be disabled.

#### Accessibility

The runtime enforces accessibility automatically:

- Each trigger button has `aria-haspopup="dialog"` and an accessible label derived from `content.triggerLabel`, the trigger's text content, or an inner image's `alt` attribute.
- If no accessible name can be derived, a fallback label of `"Open lightbox item"` is applied.
- Focus is trapped inside the lightbox overlay while open (`trapFocus: true`).
- Focus returns to the triggering element when the overlay closes (`returnFocus: true`).
- Invalid items (missing trigger or full content) are marked with `data-ome-invalid="true"` and their triggers are disabled with `aria-disabled="true"`.

---

### Lightbox Item

Each `LightboxItem` represents a single gallery entry. It provides two slots — **trigger** (the clickable preview) and **full** (the content shown in the overlay). Items are ordered by their position within the root; arrow key navigation follows this order.

<PropsTable group="Content">
  <Prop name="triggerLabel" type="string" defaultValue='""'>
    Accessible label for the trigger button. Also used as fallback visible text when the trigger slot is empty. If omitted and the trigger slot contains content, the label is inferred from the trigger's text or image `alt` attribute.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-lightbox-item-default">
    CSS class applied to the item wrapper element.
  </Prop>
  <Prop name="triggerClass" type="class" defaultValue="ome-lightbox-item-default__trigger">
    CSS class applied to the clickable trigger button.
  </Prop>
  <Prop name="fullClass" type="class" defaultValue="ome-lightbox-item-default__full">
    CSS class applied to the hidden full-content wrapper.
  </Prop>
</PropsTable>

#### Slot Rules

Every useful `Lightbox Item` needs content in the **full** slot — that is the content the overlay displays. The **trigger** slot can be custom markup (an image, text, icon, etc.) or fallback text from `content.triggerLabel`.

#### Trigger Fallback Rendering

When the trigger slot is empty and `content.triggerLabel` has a value, the item renders the label text as the visible trigger button content:

```
<button aria-label="Open image one">
  Open image one
</button>
```

When the trigger slot has content, the `content.triggerLabel` is used only as the `aria-label` for accessibility.

#### Invalid Item Handling

If an item is missing its trigger or full container, the runtime marks it as invalid:

- `data-ome-invalid="true"` is set on the item element.
- The trigger button is disabled (`disabled` + `aria-disabled="true"`).
- The trigger cursor changes to `not-allowed` via the fixed styling.
- The item is skipped during gallery initialization.

---

## CSS Custom Properties

| Property | Default | Used By | Description |
|---|---|---|---|
| `--ome-lightbox-trigger-gap` | `var(--space-xs, 0.5rem)` | Trigger button children | Controls the gap between inline-flex children inside the trigger button. |
| `--ome-lightbox-html-padding` | `24px` | HTML-mode slides | Viewport padding around HTML-mode slide content. Set from `presentation.htmlViewportPadding`. |

### PhotoSwipe CSS Custom Properties

The following PhotoSwipe variables are set by the runtime and can be overridden in your own CSS:

| Property | Default | Description |
|---|---|---|
| `--pswp-bg` | `#000` | Overlay background color. |
| `--pswp-placeholder-bg` | `#222` | Placeholder background while images load. |
| `--pswp-root-z-index` | `2147483647` | Z-index for the PhotoSwipe root element. |
| `--pswp-icon-color` | `#fff` | Icon (close, arrows, zoom) fill color. |
| `--pswp-icon-color-secondary` | `#4f4f4f` | Secondary icon color. |
| `--pswp-icon-stroke-color` | `#4f4f4f` | Icon stroke color. |
| `--pswp-icon-stroke-width` | `2px` | Icon stroke width. |
| `--pswp-error-text-color` | `var(--pswp-icon-color)` | Error message text color. |
| `--pswp-transition-duration` | _(PhotoSwipe default)_ | Transition duration for UI element fade. |

---

## Common Mistakes

<CommonMistake title="Leaving the full slot empty">
  The full slot is the content the overlay displays. If it is empty, the item will be marked as invalid and skipped during gallery initialization. Always add content to the full slot.
</CommonMistake>

<CommonMistake title="Missing width and height on full-slot images">
  When using image mode, the full-slot `` must have explicit `width` and `height` attributes. Without deterministic dimensions, the runtime falls back to HTML mode, which loses PhotoSwipe's native pinch-to-zoom and responsive sizing. A console warning is logged when this fallback occurs.
</CommonMistake>

<CommonMistake title="Expecting the trigger slot to also be the overlay content">
  The trigger and full slots are separate by design. The trigger is what users click to open the lightbox. The full slot is what appears inside the overlay. They are not automatically shared or duplicated.
</CommonMistake>

<CommonMistake title="Wrapping Lightbox Items in an extra container">
  Do not place another block between `Lightbox` and `Lightbox Item`. The runtime scans for direct `[data-ome-lightbox-item]` descendants of the root to build the gallery. Extra wrappers between the root and items will still work (the query uses `querySelectorAll`), but the positional index used for gallery navigation is based on DOM order — keep items as close to the root as possible for predictable behavior.
</CommonMistake>

<CommonMistake title="Expecting a separate media-src prop API">
  The Lightbox family is slot-driven. There is no separate prop for specifying the full-size image URL — place the image in the full slot and the runtime extracts `src`, `srcset`, dimensions, and alt text automatically.
</CommonMistake>

---

## FAQs

<details>
<summary>How does image mode detection work?</summary>

The runtime checks if both the trigger slot and the full slot each contain exactly **one** `` element. If they do, and the full-slot image has explicit `width` and `height` attributes, it enters image mode. The full-slot image's `src` (or `data-src` / `data-lazy-src` for lazy-loaded images) becomes the lightbox source, and the trigger image's `src` becomes the placeholder thumbnail during the open animation. If any of these conditions are not met, the item falls back to HTML mode.

</details>

<details>
<summary>Can I mix image items and HTML items in the same gallery?</summary>

Yes. Each item is independently detected as image mode or HTML mode. You can have image items alongside HTML items in the same Lightbox root. Arrow key navigation works across all items regardless of mode.

</details>

<details>
<summary>How do I customize the overlay appearance?</summary>

Use the `presentation.bgOpacity` prop to change the overlay darkness, and override the PhotoSwipe CSS custom properties (like `--pswp-bg`, `--pswp-icon-color`) in your own stylesheet to change colors, z-index, and other visual aspects. The `presentation.htmlViewportPadding` prop controls the padding around HTML-mode slides.

</details>

<details>
<summary>Does Lightbox support lazy-loaded images?</summary>

Yes. The runtime resolves lazy-loaded images by checking `data-src`, `data-lazy-src`, `data-srcset`, and `data-lazy-srcset` attributes as fallbacks. This is compatible with common lazy-loading plugins like WP Rocket and BJ Lazy Load. `<picture>` element sources are also resolved automatically.

</details>

<details>
<summary>Can I open the lightbox programmatically?</summary>

The lightbox instance is stored on the root element as `element._omeLightbox`. You can use `element._omeLightbox.lightbox.loadAndOpen(index, dataSource)` to open the gallery at a specific item index. The runtime also syncs `data-ome-state` (`"open"` / `"closed"`) and `data-ome-current-index` on the root element for CSS targeting.

</details>

<details>
<summary>What happens if I have only one item?</summary>

A single-item gallery works normally — the overlay opens with the item, and navigation arrows are automatically hidden by PhotoSwipe when there is only one slide.

</details>
