# JavaScript image gallery, get started
> Install lightGallery with npm or a CDN and build a responsive image and video gallery in a few lines of JavaScript, no dependencies, 15 plugins.
lightGallery is a lightweight, modular, JavaScript library for creating beautiful image & video galleries for the web and the mobile.
Canonical page: https://www.lightgalleryjs.com/docs/getting-started/
## Core features
#### Packages and build
- One gallery, four packages: `lightgallery` for plain JavaScript and
TypeScript, plus native [React](/docs/react/), [Vue 3](/docs/vue/) and
[Angular](/docs/angular/) packages that render their own DOM.
- A shared [headless core](/docs/headless/) behind every package, so
settings and behavior are identical in each stack.
- Zero dependencies, TypeScript types, ES module and UMD builds.
The core is about 19 KB minified and gzipped; plugins are separate
entries and cost nothing until you import them.
- Modular architecture with 15 built-in plugins, and an API for
[writing your own](/docs/creating-plugins/).
- Easily customizable via CSS (SCSS) and settings. Multiple instances
on one page.
#### Layout and performance
- Fully responsive, with [srcset, sizes and picture
sources](/docs/responsive-loading/) on every slide.
- [Justified layout](/docs/justified-layout/) plugin: row-justified
trigger grids with no layout library in front of the lightbox.
- [Virtualization](/docs/virtualization/) keeps a small, constant DOM
for galleries with thousands of slides.
- [Dynamic mode](/demos/dynamic-mode/) opens a gallery from an array
of items, and [live updates](/demos/update-slides/) add, edit or
remove slides while it is open.
- Inline gallery and carousel modes.
- Smart preloading of neighboring slides.
#### Interaction
- Highly optimized for touch devices, with mouse drag on desktops.
- Gesture physics: every release lands in a spring seeded with your
gesture's velocity, flings glide, edges resist with friction.
- Swipe, drag or pinch to close.
- Zoom in and out, pinch to zoom, double-click or double-tap for actual
size. Slides [open from the thumbnail's
position](/demos/zoom-from-origin/) and close back to it.
- Animated thumbnails, with [thumbnail
scrubbing](/docs/thumbnail-scrubbing/) to drag through the gallery.
- 20+ hardware-accelerated CSS3 transitions, plus your own easing.
- Keyboard navigation and full screen support.
- A toolbar that fits: on narrow screens the lowest-priority buttons
move into a "More options" menu.
- [Accessibility](/docs/accessibility/): modal dialog semantics, focus
trap, screen-reader announcements, reduced-motion support.
- [Localization and RTL](/docs/localization-rtl/): every label is a
setting, and the gallery mirrors for right-to-left pages.
#### Media and plugins
- YouTube, Vimeo, Wistia and HTML5 video, with [video
facades](/docs/video-facades/) that load the player only on play.
- HTML iframe support and [mixed content](/demos/mixed-contents/):
photos, videos, maps and PDFs in one gallery.
- HTML [captions](/demos/captions/) per slide, with animated and
relative-position variants.
- Rotate and flip images.
- [Sharing](/docs/web-share/) through the native share sheet where it
exists, social links where it does not.
- [Deep links](/docs/hash-drivers/) with browser back and forward,
through the Navigation API or the History API.
- Autoplay slideshow with a progress bar, pager dots, per-slide
comments, and a [medium-zoom](/demos/medium-zoom/) plugin for
in-page image zoom.
- Inline SVG icons, each [replaceable by name](/docs/custom-icons/).
## Ask your coding agent
Working with a coding agent? Paste the prompt below into it and replace
the first line with the gallery you want. It points the agent at the
markdown version of these docs, so it picks the right package for your
stack and avoids the setup mistakes behind most bug reports.
```text
Add lightGallery to this project: [describe the gallery you want, for
example "a product photo grid that opens a lightbox with thumbnails and
zoom"].
1. Detect the stack and use the matching package:
- React: @lightgallery/react
- Vue 3: @lightgallery/vue
- Angular: @lightgallery/angular
- Anything else (plain JavaScript, TypeScript, server-rendered HTML):
lightgallery
2. Before writing code, read the docs index at
https://www.lightgalleryjs.com/llms.txt and the guide for that stack:
- React: https://www.lightgalleryjs.com/docs/react/index.md
- Vue: https://www.lightgalleryjs.com/docs/vue/index.md
- Angular: https://www.lightgalleryjs.com/docs/angular/index.md
- Plain JavaScript:
https://www.lightgalleryjs.com/docs/getting-started/index.md
Use the install command from that guide exactly. The framework
packages also need the lightgallery package, which ships the CSS.
3. Import the CSS yourself: lightgallery/css/lightgallery.css plus one
file per plugin you use, such as lightgallery/css/lg-thumbnail.css.
The framework packages ship no styles.
4. Plugins are opt-in. Import each one from its plugins/ entry and
add it to the plugins list (in Angular, the features input with
withThumbnail(), withZoom() and so on). A plugin's settings do
nothing until the plugin is added. Unless I ask for something else,
include the zoom and thumbnail plugins and their CSS files.
5. Give every item the full-size image URL, a thumbnail and alt text.
Set lgSize to "WIDTH-HEIGHT" (data-lg-size in HTML markup) on every
image item, using the natural size of the full image; read it from
the image files, the image optimizer or the CMS. Without it the slide
fades in instead of opening from its thumbnail.
6. Check every option name against
https://www.lightgalleryjs.com/docs/settings/index.md instead of
guessing. Commercial projects also need the licenseKey setting.
7. When you are done, run the project's build or type check, open the
gallery in a browser, and confirm the styles are applied and the
console shows no errors.
```
## Installation
lightGallery is available on NPM, Yarn, CDNs, and GitHub. You can use any
of the following methods to download lightGallery
- [NPM](https://www.npmjs.com/) - NPM is a package manager for the JavaScript
programming language. You can install `lightgallery` using the following
command
```sh
npm install lightgallery
```
- [YARN](https://yarnpkg.com/) - Yarn is another popular package manager for
the JavaScript programming language. If you prefer you can use Yarn instead
of NPM
```sh
yarn add lightgallery
```
- [GitHub](https://github.com/sachinchoolur/lightGallery/releases) -
You can also download a release directly from GitHub
- CDN - If you prefer to use a CDN, you can load files via
[jsdelivr](https://www.jsdelivr.com/package/npm/lightgallery),
[cdnjs](https://cdnjs.com/libraries/lightgallery) or
[unpkg](https://unpkg.com/browse/lightgallery@latest/)
#### Include CSS and Javascript files
First of all, include lightgallery.css in the <head> of the document. If you
want to include any lightGallery plugin such as thumbnails or zoom, you need to
include respective css files as well.
Alternatively you can include `lightgallery-bundle.css` which contains
lightGallery and all plugin styles instead of separate stylesheets.
If you like you can also import scss files instead of css files from the `scss`
folder.
```html
```
Then include `lightgallery.umd.js` into your document. If you want to include
any lightgallery plugin you can include it after `lightgallery.umd.js`.
```html
....
```
lightGallery supports AMD, CommonJS and ES6 modules too.
```javascript
import lightGallery from 'lightgallery';
// Plugins
import lgThumbnail from 'lightgallery/plugins/thumbnail'
import lgZoom from 'lightgallery/plugins/zoom'
```
#### The markup
lightGallery does not force you to use any kind of markup. You can use whatever
markup you want. Here you can find
detailed examples of different kinds of markups.
Give each anchor the natural size of its full image via
`data-lg-size="${width}-${height}"`. It drives the
[zoom-from-origin](/docs/settings/#zoomFromOrigin) animation: the slide opens
from the clicked thumbnail and closes back to it. Without the attribute the
slide fades in instead.
```html
```
#### Initialize lightGallery
Finally, you need to initiate the gallery by adding the following code.
```html
```
#### License Key
You'll receive a license key via email once you purchase a license [More info](https://www.lightgalleryjs.com/docs/settings/#licenseKey)
#### Plugins
As shown above, you need to pass the plugins via settings if you want to use any
lightGallery plugins.
If you are including lightGallery files via script tag, please use the same
plugins names as follows.
`lgZoom`, `lgAutoplay`, `lgComment`, `lgFullscreen`, `lgHash`, `lgJustified`,
`lgPager`, `lgRelativeCaption`, `lgRotate`, `lgShare`, `lgThumbnail`, `lgVideo`,
`lgVimeoThumbnail`, `lgMediumZoom`, `lgOriginCrop`
## Browser support
lightGallery supports current evergreen browsers and iOS Safari: Chrome,
Edge, Firefox and Safari on desktop, Safari on iOS and Chrome on Android.
The builds target ES2017 and run without transpiling or polyfills.
- **ES module and UMD builds.** Import it in a bundler, load it as a
native `
```
Prefer to own the state? Pass `:slides` with `v-model:open` and
`v-model:index`. Both modes are documented in the
[Vue package docs](/docs/vue/).
## What you get
- **All 14 plugins** as tree-shakable subpaths, thumbnails, zoom, video,
autoplay, fullscreen, hash deep links, pager, share, rotate, comments,
medium-style zoom, relative captions, Vimeo thumbnails and origin crop,
plus the justified layout.
- **Idiomatic Vue API**: settings are same-named props (`:speed`,
`:loop`, `:caption-position`), per-plugin settings are one object prop
per plugin, events are kebab-case emits (`@after-slide`), and the
gallery chrome comes from named scoped slots (`#caption`, `#counter`,
`#prev-button`, `#next-button`).
- **Typed end to end**, including `LgGalleryItem` for slide data and a
template-ref handle for imperative control.
- **Nuxt-ready**: entries import without browser globals and the overlay
never server-renders, so no `` wrapper and no hydration
mismatch.
- **Accessibility**: dialog semantics, focus trap and restore, keyboard
operable thumbnails and pager, `prefers-reduced-motion` support; axe
reports zero violations.
## Next steps
- [Vue package docs](/docs/vue/), the full prop surface, all plugins,
SSR and accessibility notes.
- [Vue image gallery demo](/demos/vue-image-gallery/) and
[Vue video gallery demo](/demos/vue-video-gallery/), live galleries
with the code behind them.
- [Settings reference](/docs/settings/), every setting, shared by all
four packages.
> **Using the 2.x wrapper?** The old `lightgallery/vue` component is
> documented in the [2.x Vue archive](/docs/v2/vue/), and the
> [migration guide](/docs/migration/) covers the move to the native package.
---
# lightGallery facts and figures
> The numbers behind lightGallery: bundle sizes for the core, every plugin and the framework packages, dependencies, supported browsers and framework versions, media types, accessibility, license and prices.
One page of checkable facts: how big lightGallery is, what it depends on, what it runs in, what it plays, and what it costs. Sizes are measured from the build this site was made with.
Canonical page: https://www.lightgalleryjs.com/docs/facts/
lightGallery is an open-source JavaScript lightbox and gallery for images
and video, with native React, Vue and Angular packages built on one
shared core, no dependencies, and every feature outside the core shipped
as an opt-in plugin. This page is the reference for the numbers; the
figures that come from the build are measured when the site is built, so
they match the version in the heading below.
## What is the current version?
| | |
| --- | --- |
| Version | |
| First 3.0 release | 9 October 2026 |
| Packages | `lightgallery`, `@lightgallery/headless`, `@lightgallery/react`, `@lightgallery/vue`, `@lightgallery/angular` |
| Versioning | The five packages always ship together under one version number |
| Started | 2014, as a jQuery plugin; dependency-free since version 2 |
| Language | TypeScript, with type declarations in every package |
## How big is lightGallery?
The core is the lightbox itself. Thumbnails, zoom, video and the rest
are plugins with their own entry points, so a gallery pays only for the
features it imports. A typical image gallery with thumbnails and zoom is
the core plus two plugins, about 30 KB of JavaScript gzipped.
## What does it depend on?
| Package | Runtime dependencies |
| --- | --- |
| `lightgallery` | None |
| `@lightgallery/headless` | None |
| `@lightgallery/react` | `@lightgallery/headless` |
| `@lightgallery/vue` | `@lightgallery/headless` |
| `@lightgallery/angular` | `@lightgallery/headless`, `tslib`, and the Angular CDK as a peer |
jQuery is not required anywhere. The stylesheets come from the
`lightgallery` package and are shared by every binding.
## Which frameworks and versions does it support?
| Package | Framework range | Server rendering |
| --- | --- | --- |
| `lightgallery` | Plain JavaScript or TypeScript, ES module and UMD builds, CDN script tags | Markup renders on the server; the gallery initialises in the browser |
| `@lightgallery/react` | React 18 and 19 | Yes: trigger grids render on the server, the lightbox mounts after hydration, no dynamic import needed |
| `@lightgallery/vue` | Vue 3.4 and later | Yes, including Nuxt |
| `@lightgallery/angular` | Angular 21 and 22, standalone components, signal inputs, zoneless | Yes, including Angular SSR with hydration |
| `@lightgallery/headless` | Any framework: the state machine, settings, gesture, zoom and thumbnail maths with no DOM | Not applicable |
Node 18 or later is required for the build tooling; the browser bundles
have no Node requirement.
## Which browsers does it run in?
Current evergreen browsers: Chrome, Edge, Firefox and Safari on desktop,
Safari on iOS and Chrome on Android. The builds target ES2017 and run
without transpiling or polyfills. Internet Explorer is not supported.
## What media does it show?
| Type | How |
| --- | --- |
| Images | `src`, `srcset` and `sizes`, and `` sources for art direction and format negotiation |
| YouTube, Vimeo and Wistia | By URL. Provider slides load as a poster facade; the provider iframe is created only when the visitor presses play. YouTube uses the privacy-enhanced no-cookie host by default |
| HTML5 video | MP4, WebM and other formats the browser plays, with poster images |
| Iframes | Any page, map or PDF viewer |
| Inline HTML | Any markup, as a slide |
| Dynamic content | Slides built from a JavaScript array, added and removed while the gallery is open |
## How much of the API is there?
| | Count |
| --- | --- |
| Settings | 158, across the core and 15 plugins |
| Events | 22 |
| Methods | 8 |
| Plugins | 15 |
| Slide transitions | 20 and more, switched with one setting |
The [settings](/docs/settings/), [events](/docs/events/) and
[methods](/docs/methods/) references are generated from the source, and
the same data is served as JSON at `/api/settings.json`,
`/api/events.json` and `/api/methods.json`.
## Is it accessible?
The open gallery is a modal dialog with `role="dialog"` and
`aria-modal`, focus moves into it on open, Tab is trapped while it is
open and focus returns to the trigger on close. Slide changes are
announced to screen readers, the controls are labelled and translatable,
and `prefers-reduced-motion` disables the animations. All four packages
run axe checks against WCAG 2.0 and 2.1 A and AA rules in continuous
integration. The [accessibility page](/docs/accessibility/) has the full
contract.
## Is lightGallery free?
Yes. lightGallery is free and open source under the GPLv3, for personal
and commercial projects alike, as long as the project is distributed
under GPLv3-compatible terms.
## How much does the commercial license cost?
A project that keeps its source proprietary needs a commercial license.
It is a one-time payment, the code and every plugin are the same, and
there are no feature gates.
| License | Price (USD, one-time) | Covers |
| --- | --- | --- |
| Hobby | 48 | One developer, one product, not for a product that is sold |
| Team | 118 | Up to eight developers, up to five products, products for sale allowed |
| Organization | 188 | Unlimited developers and products, products for sale allowed |
| Extended | 239 | Unlimited developers, one product that itself produces copies, such as an SDK, site builder, theme or template |
Open-source projects under a GPLv3-compatible license can request a free
key. The [license page](/license/) has the purchase links and the full
terms.
## Does a version 2 license cover version 3?
No. A license covers every release within the major version it was
bought for, so a 2.x license covers all 2.x releases and version 3 needs
a new license. Licenses bought in the six months before the 3.0 release
can be exchanged by email. Version 3 keys start with `LIG`; an older key
keeps the gallery working and logs a console warning.
## How widely is it used?
| | Figure |
| --- | --- |
| npm downloads of `lightgallery` | About 90,000 a week in October 2026 |
| GitHub | About 7,000 stars |
| CDN | Served by jsDelivr, cdnjs and unpkg |
## Where do I get it?
| | |
| --- | --- |
| npm | `npm install lightgallery`, or the framework package plus `lightgallery` for the stylesheets |
| CDN | `https://cdn.jsdelivr.net/npm/lightgallery@3/lightgallery.min.js` and `/css/lightgallery.css`, the same paths on cdnjs and unpkg; `@3` follows the latest 3.x release |
| Source | [github.com/sachinchoolur/lightGallery](https://github.com/sachinchoolur/lightGallery) |
| For coding agents | [llms.txt](/llms.txt), a markdown twin of every docs page at its URL plus `index.md`, and the [agent skill](https://github.com/sachinchoolur/lightGallery/tree/main/skills/lightgallery) |
The [getting started guide](/docs/getting-started/) takes it from here.
---
# Image and video gallery for Angular
> Native Angular image and video gallery component, standalone, signal inputs, zoneless, CDK overlay, thumbnails, pinch zoom and SSR support.
Full featured image and video lightbox gallery component for Angular.
Canonical page: https://www.lightgalleryjs.com/docs/angular-image-video-gallery/
lightGallery 3 ships **`@lightgallery/angular`**, a native Angular
component, not a wrapper. It is a standalone component with signal inputs
and outputs, zoneless change detection and CDK overlay/a11y, published in
Angular Package Format with one secondary entry point per feature. State and
gallery logic come from the shared headless core, so behavior matches the
vanilla, React and Vue packages exactly.
## Install
```bash
npm install @lightgallery/angular @angular/cdk lightgallery
```
Peer ranges are `@angular/core`, `@angular/common`, `@angular/platform-browser`
and `@angular/cdk` `>=21 <23`. There is no `zone.js` in the package. Import
the stylesheets in your root stylesheet, or add the same paths to the
`styles` array in `angular.json`:
```css
/* src/styles.css */
@import 'lightgallery/css/lightgallery.css';
@import 'lightgallery/css/lg-thumbnail.css';
@import 'lightgallery/css/lg-zoom.css';
```
## A gallery in one component
```ts
import { Component } from '@angular/core';
import {
LgGalleryComponent,
LgGalleryItemDirective,
type LgGalleryItem,
} from '@lightgallery/angular';
import { withThumbnail } from '@lightgallery/angular/plugins/thumbnail';
import { withZoom } from '@lightgallery/angular/plugins/zoom';
@Component({
selector: 'app-gallery',
imports: [LgGalleryComponent, LgGalleryItemDirective],
template: `
@for (item of items; track item.src) {
}
`,
})
export class Gallery {
features = [withThumbnail({ thumbWidth: 120 }), withZoom({ scale: 1.5 })];
items: LgGalleryItem[] = [
{
src: 'img/1-1600.jpg',
thumb: 'img/1-240.jpg',
alt: 'Mountains',
lgSize: '1600-1067',
caption: 'Mountains',
},
];
}
```
Prefer to own the state? Pass `[slides]` with `[open]`/`(closed)` and
`[(index)]`. Both modes are documented in the
[Angular package docs](/docs/angular/).
## What you get
- **All 14 features** as tree-shakable entry points with `with()`
factories, thumbnails, zoom, video, autoplay, fullscreen, hash deep
links, pager, share, rotate, comments, medium-style zoom, relative
captions, Vimeo thumbnails and origin crop, plus the justified layout.
- **Idiomatic Angular API**: settings are same-named signal inputs
(`[speed]`, `[loop]`, `[captionPosition]`), events are outputs without
the `on` prefix (`(afterSlide)`), and the gallery chrome comes from
`ng-template` directives (`lgCaption`, `lgCounter`, `lgPrevButton`,
`lgNextButton`).
- **Zoneless-ready** and fully typed, including `LgGalleryItem` for slide
data and an `exportAs` handle for imperative control.
- **SSR-safe**: entry points import cleanly in bare Node and the overlay
mounts after hydration, so trigger markup server-renders and galleries
stay crawlable.
- **Accessibility**: dialog semantics via CDK, focus trap and restore,
keyboard operable thumbnails and pager, `prefers-reduced-motion`
support.
## Next steps
- [Angular package docs](/docs/angular/), the full input surface, all
features, SSR and accessibility notes.
- [Angular image gallery demo](/demos/angular-image-gallery/) and
[Angular video gallery demo](/demos/angular-video-gallery/), live
galleries with the code behind them.
- [Settings reference](/docs/settings/), every setting, shared by all
four packages.
> **Using the 2.x wrapper?** The old `lightgallery/angular` component is
> documented in the [2.x Angular archive](/docs/v2/angular/), and the
> [migration guide](/docs/migration/) covers the move to the native package.
---
# lightGallery license
> lightGallery is free and open source under the GPLv3. The commercial license is for projects that keep their source proprietary: same code, every plugin, one-time payment.
Which license applies to your project, how to pass your license key, and what the temporary key does. Plans and pricing are on the [license page](https://www.lightgalleryjs.com/license/).
Canonical page: https://www.lightgalleryjs.com/docs/license/
## Open source license
lightGallery is released under the
[GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
It is free to use in any project, personal or commercial, as long as that
project is distributed under GPLv3-compatible terms. The
[GPL FAQ](https://www.gnu.org/licenses/gpl-faq.html) explains what that means
for your own code.
Open-source projects can request a license key at
[contact@lightgalleryjs.com](mailto:contact@lightgalleryjs.com) so the
gallery runs without the console notice.
## Commercial license
If you would rather keep your own source proprietary, the commercial license
is for you. It covers the same code, with every feature and official plugin
included; nothing is gated behind it. Plans are one-time payments, priced by
team size, and come with a license key by email. See
[plans and pricing](/license/).
## Extended commercial license
If you want to include lightGallery as part of a software development kit
(SDK), web application builder or website builder, downloadable or
installable products like WordPress themes, HTML templates, or something
that produces copies that each use lightGallery, you need the Extended
Commercial license.
## Frequently asked
**Is lightGallery free?** Yes. Under the GPLv3 every package and every
plugin is free, for personal and commercial projects alike, when your project
is distributed under GPLv3-compatible terms.
**When do I need a commercial license?** When you keep your source
proprietary, so releasing it under GPLv3-compatible terms is not an option.
The kind of project does not decide this; the terms your own code ships
under do.
**Does the commercial license add features?** No. Both licenses cover the
same code. The license key only turns off the console notice.
**Can I try it before buying?** Yes. The temporary key below runs every
feature and plugin; it only logs a console warning that it is not for
production use.
**I ship a theme, template or site builder. Which license?** Products that
produce copies, each using lightGallery, need the Extended Commercial
license.
## License key
Pass your key with the [`licenseKey`](/docs/settings/#licenseKey) setting:
```js
lightGallery(document.getElementById('lightgallery'), {
licenseKey: 'your_license_key',
});
```
- **Commercial licenses** come with a key by email once you purchase.
lightGallery 3 keys start with `LIG`.
- **Upgrading from v1 or v2?** Those keys don't start with `LIG` and
aren't valid for lightGallery 3. The gallery keeps working and logs a
console warning asking you to upgrade; see the
[upgrade note on the license page](/license/).
- **Open source projects** under a GPLv3-compatible license can get a key
at [contact@lightgalleryjs.com](mailto:contact@lightgalleryjs.com).
- **Trying it out?** Use `0000-0000-000-0000` as a temporary key. It
doesn't limit anything: every feature and plugin works exactly as it
does with a purchased key. The only difference is a console warning
that the key is not for production use, so if you see an error, it
comes from something else, not from licensing.
### One key for every gallery on the page
With several galleries on a page, set the key once with `setLicenseKey`
before creating them. A gallery's own `licenseKey` still wins, and each
license message is logged once per page, not once per gallery.
```js
// Vanilla JavaScript (script tag: window.lightGallery.setLicenseKey)
import lightGallery from 'lightgallery';
lightGallery.setLicenseKey('your_license_key');
```
```ts
// React, Vue or Angular: once at app start, for example in main.ts
import { setLicenseKey } from '@lightgallery/react'; // or /vue, /angular
setLicenseKey('your_license_key');
```
The temporary key is also the default, so a gallery without a
`licenseKey` shows the same warning. An empty `licenseKey` logs a console
error pointing here instead; the gallery still runs.
---
# Migrating to lightGallery 3
> Upgrade guide for lightGallery 3, changed defaults and settings for vanilla users, and wrapper-to-native tables for React, Vue and Angular.
What changes when you upgrade, framework by framework. Vanilla galleries keep working as documented; the framework wrappers are replaced by native packages.
Canonical page: https://www.lightgalleryjs.com/docs/migration/
lightGallery 3 restructures the project around a framework-free core plus
native packages for React, Vue and Angular. The vanilla library keeps its
runtime and its API; the framework wrappers, which embedded that runtime
and mirrored its DOM, are replaced by components that render natively.
| You were using | You now install |
| --- | --- |
| `lightgallery` | `lightgallery` (unchanged) |
| `lightgallery/react` | [`@lightgallery/react`](/docs/react/) |
| `lightgallery/vue` | [`@lightgallery/vue`](/docs/vue/) |
| `lightgallery/angular` | [`@lightgallery/angular`](/docs/angular/) |
## Vanilla JavaScript
Version 3 is a complete rewrite, but the API is unchanged. The upgrade is
packaging, a handful of new settings, a few changed defaults and four
removals.
- **Imports are unchanged**, `import lightGallery from 'lightgallery'`,
plugins from `lightgallery/plugins/`, CSS from
`lightgallery/css/.css`. The package now has proper export maps,
so bundlers and Node resolve subpaths without deep-path workarounds.
The CDN paths are unchanged too: the package root is still the build
folder, so `css/lightgallery.css` and `plugins/zoom/lg-zoom.umd.js` are
where 2.x had them on jsDelivr, unpkg and cdnjs.
- **The icon font is gone.** Controls render inline SVG icons, and the
`fonts/lg.*` files and their `@font-face` rule no longer ship. If you
self-hosted the font or restyled icons through font glyphs on
`.lg-icon`, replace them with [custom icons](/docs/custom-icons/).
- **No Internet Explorer.** The ES module and UMD builds target ES2017
for current evergreen browsers and iOS Safari, with no ES5 build (the
ES module keeps its historical `*.es5.js` file name). See
[browser support](/docs/getting-started/#browser-support); v2 remains
available for IE.
- **`supportLegacyBrowser` is gone.** It only hooked up a polyfill
for `srcset` and ``, which every supported browser handles
natively. Remove the setting from your options; TypeScript
reports it as an unknown property. The `CustomEvent` and
`Element.matches` polyfills the library used to install on the page
are removed with it.
- **The share dropdown has no overlay.** It dismisses like the
toolbar's More options menu: on a press anywhere outside it, on
Escape and when focus tabs out of it. The `.lg-dropdown-overlay`
element that dimmed the gallery behind it is not rendered, so drop
any CSS that targeted it. See
[Web Share](/docs/web-share/#dismissing-the-dropdown).
- **New settings keep the 2.x behavior by default**, except the ones
listed under [changed defaults](#changed-defaults) below:
[`strings`](/docs/localization-rtl/) and
[`direction`](/docs/localization-rtl/),
[`virtualization`](/docs/virtualization/),
[`hashDriver`](/docs/hash-drivers/),
[`videoFacade`](/docs/video-facades/),
[`preferNativeShare`](/docs/web-share/),
the [justified layout](/docs/justified-layout/) plugin,
`ariaAnnouncements`, `flickVelocity` and `pinchToClose`.
### Changed defaults
| Setting | 2.x | 3.0 | Effect |
| --- | --- | --- | --- |
| `videoFacade` | n/a (eager iframe) | `true` | Provider video slides render a poster with a play button; the iframe mounts on play. Set `false` for the old behavior |
| `youTubeNoCookie` | n/a (`youtube.com`) | `true` | YouTube embeds go through `youtube-nocookie.com`. URLs that already point there always keep it |
| `preferNativeShare` | n/a | touch: `true`, desktop: `false` | On touch devices the share button opens the system share sheet, with the branded dropdown as the fallback |
| `hashDriver` | n/a (History API) | `'auto'` | Deep links use the Navigation API where the browser has it, History everywhere else. Force the old engine with `'history'` |
| `toolbarOverflow` | n/a (buttons wrap) | `true` | When the toolbar buttons do not fit beside the counter, the lowest-priority ones move into a "More options" menu. Set `false` to let them wrap onto a second row as before |
| `showGestureButtons` | n/a (always shown) | desktop: `true`, touch: `false` | Touch devices leave out the zoom in, zoom out and actual-size buttons, since pinch and double-tap do the same. Set `mobileSettings: { showGestureButtons: true }` to show them |
`direction` stays `'ltr'`, so nothing mirrors until you opt in with
`'rtl'` or `'auto'`.
### Strings are unified
Every user-facing label, core controls *and* plugin buttons, now lives in
one `strings` object:
```js
lightGallery(el, {
plugins: [lgZoom, lgThumbnail],
strings: {
closeGallery: 'Galerie schließen',
nextSlide: 'Nächstes Bild',
zoomIn: 'Vergrößern',
toggleThumbnails: 'Miniaturen umschalten',
},
});
```
The seven per-plugin objects still work and are **deprecated**:
`autoplayPluginStrings`, `commentPluginStrings`, `fullscreenPluginStrings`,
`rotatePluginStrings`, `sharePluginStrings`, `thumbnailPluginStrings` and
`zoomPluginStrings`. A legacy key you set explicitly still wins over the
`strings` default, so mixed configurations keep working while you move
across. The full key list is in the
[localization guide](/docs/localization-rtl/).
## License keys
lightGallery 3 keys start with `LIG`. A v1 or v2 key is not valid for
version 3: the gallery keeps working and logs a console warning asking
you to upgrade. The [upgrade note on the license page](/license/)
explains how to get a v3 key, and the [license docs](/docs/license/)
cover open-source use and the temporary key for evaluation.
## React
The 2.x component wrapped the vanilla runtime; `@lightgallery/react` renders
every node itself. The archived wrapper docs stay at
[/docs/v2/react/](/docs/v2/react/).
```bash
npm install @lightgallery/react lightgallery
```
Keep `lightgallery` installed, the CSS still ships from it, unchanged.
| 2.x wrapper | 3.0 native package |
| --- | --- |
| `import LightGallery from 'lightgallery/react'` | `import { LightGallery } from '@lightgallery/react'` |
| Anchor children scraped for `data-*` attributes | `` children, or a `slides` array |
| `dynamic` + `dynamicEl={items}` | `slides={items}` |
| `elementClassNames` | `className` |
| `onInit={({ instance }) => …}` then calling methods on `instance` | `ref` handle: `gallery.current.openGallery(0)` |
| Plugin settings flat on the component (`thumbWidth={130}`) | One object prop per plugin (`thumbnail={{ thumbWidth: 130 }}`) |
| `subHtml` HTML strings | `caption` React nodes (`captionHtml` for raw HTML) |
| `plugins={[lgThumbnail]}` from `lightgallery/plugins/*` | `plugins={[Thumbnail]}` from `@lightgallery/react/plugins/*` |
Lifecycle callbacks keep their documented names (`onBeforeSlide`,
`onAfterSlide`, `onSlideItemLoad`, …) and their detail payloads. Slide data
uses the same field names, with `size` renamed to `lgSize`.
Two behaviors are worth knowing about:
- **Controlled mode is available**: `open`, `index`, `onClose` and
`onIndexChange` let React own the state, the wrapper had no equivalent.
- **Updating slides needs no `refresh()`**: rendering different children
or a different `slides` array is the update. The `refresh()` method
remains on the handle for API parity.
## Vue
The 2.x wrapper is documented at [/docs/v2/vue/](/docs/v2/vue/).
`@lightgallery/vue` needs `vue >=3.4`.
```bash
npm install @lightgallery/vue lightgallery
```
| 2.x wrapper | 3.0 native package |
| --- | --- |
| `import Lightgallery from 'lightgallery/vue'` | `import { LightGallery, LgItem } from '@lightgallery/vue'` |
| Anchor children with `data-*` attributes | `` children, or a `:slides` array |
| `:settings="{ speed: 500, plugins }"`, one settings object | Core settings as individual props (`:speed="500"`), `:plugins` on its own |
| `:onBeforeSlide="handler"` props | Kebab-case emits: `@before-slide`, `@after-slide`, … |
| Plugin settings inside `settings` | One object prop per plugin (`:zoom="{ scale: 1.5 }"`) |
| `subHtml` HTML strings | `caption` values, or the `#caption` scoped slot |
`v-model:open` and `v-model:index` are new, as are the `#caption`,
`#counter`, `#prev-button` and `#next-button` scoped slots and the
template-ref handle (`openGallery`, `closeGallery`, `goToSlide`,
`nextSlide`, `prevSlide`, `refresh`).
## Angular
The 2.x wrapper is documented at [/docs/v2/angular/](/docs/v2/angular/).
`@lightgallery/angular` requires `@angular/core`, `@angular/common`,
`@angular/platform-browser` and `@angular/cdk` `>=21 <23`, and runs without `zone.js`.
```bash
npm install @lightgallery/angular @angular/cdk lightgallery
```
| 2.x wrapper | 3.0 native package |
| --- | --- |
| `LightgalleryModule` from `lightgallery/angular` | Standalone `LgGalleryComponent` + `LgGalleryItemDirective` in `imports` |
| `` | `` with same-named signal inputs (`[speed]`, `[loop]`, …) |
| `settings.plugins: [lgZoom]` | `[features]="[withZoom()]"` from `@lightgallery/angular/plugins/zoom` |
| `settings.dynamicEl` | `[slides]`, or `[lgGalleryItem]` triggers |
| `[onBeforeSlide]="handler"` inputs | `(beforeSlide)` outputs, no `on` prefix |
| `subHtml` HTML strings | `caption` values, or the `lgCaption` `ng-template` |
Two-way `[(index)]`, `[open]`/`(closed)`, the `ng-template` slots and the
`#lg="lgGallery"` handle are new.
## Behavior parity
All four packages share the same headless core, so settings, gesture
thresholds and plugin semantics match everywhere. If a gallery behaves
differently after migrating, that is a bug worth
[reporting](https://github.com/sachinchoolur/lightGallery/issues), please
include which package and which setting.
---
# Troubleshooting
> Fixes for the lightGallery problems behind most bug reports: missing styles, plugins that do nothing, empty slides, videos that will not play, Next.js errors, license warnings and import errors after upgrading, for vanilla JavaScript, React, Vue and Angular.
Symptom by symptom, the cause and the fix, in every package. Three mistakes account for most reports: the CSS is not imported, the plugin is not registered, or the selector does not match your markup.
Canonical page: https://www.lightgalleryjs.com/docs/troubleshooting/
Each section below is one symptom. The answer comes first, then the fix
for each package. If your problem is not here, the
[Discussions board](https://github.com/sachinchoolur/lightGallery/discussions)
and the [issue tracker](https://github.com/sachinchoolur/lightGallery/issues)
are the next stop; a minimal reproduction on StackBlitz gets the fastest
answer, and every [demo page](/demos/) has an "Open in StackBlitz" button
to start from.
## The gallery opens but has no styles
The stylesheet is not loaded. lightGallery ships its CSS as separate files
and none of the packages injects styles for you, so the slides render as a
plain list of images and the page behind keeps scrolling. Import
`lightgallery/css/lightgallery.css` plus one file per plugin, or
`lightgallery/css/lightgallery-bundle.css` for everything.
```js
// Vanilla JavaScript, bundler
import 'lightgallery/css/lightgallery.css';
import 'lightgallery/css/lg-thumbnail.css';
import 'lightgallery/css/lg-zoom.css';
```
```html
```
```tsx
// React (Next.js: app/layout.tsx or any server component)
import 'lightgallery/css/lightgallery.css';
import 'lightgallery/css/lg-thumbnail.css';
```
```js
// Vue with Nuxt: nuxt.config
export default {
css: ['lightgallery/css/lightgallery.css', 'lightgallery/css/lg-thumbnail.css'],
};
```
```json
// Angular: angular.json, under "styles"
["lightgallery/css/lightgallery.css", "lightgallery/css/lg-thumbnail.css"]
```
The framework packages depend on `lightgallery` only for these CSS files,
which is why `lightgallery` is installed next to `@lightgallery/react`,
`@lightgallery/vue` and `@lightgallery/angular`.
## Thumbnails, zoom, video or another plugin does nothing
The plugin is not registered. A feature exists only when its plugin is
passed to the gallery; its settings are ignored otherwise, silently. Pass
the plugin and import its CSS file.
```js
// Vanilla JavaScript
import lightGallery from 'lightgallery';
import lgThumbnail from 'lightgallery/plugins/thumbnail';
import lgZoom from 'lightgallery/plugins/zoom';
import 'lightgallery/css/lg-thumbnail.css';
import 'lightgallery/css/lg-zoom.css';
lightGallery(document.getElementById('gallery'), {
plugins: [lgThumbnail, lgZoom],
thumbnail: { animateThumb: true },
});
```
```tsx
// React
import { LightGallery } from '@lightgallery/react';
import Thumbnail from '@lightgallery/react/plugins/thumbnail';
import Zoom from '@lightgallery/react/plugins/zoom';
;
```
```vue
```
```ts
// Angular
import { withThumbnail } from '@lightgallery/angular/plugins/thumbnail';
import { withZoom } from '@lightgallery/angular/plugins/zoom';
// In the component: features = [withThumbnail({ animateThumb: true }), withZoom()];
// In the template:
```
With script tags, load `lightgallery.umd.js` first and each plugin file
after it; the plugins register themselves as `lgThumbnail`, `lgZoom` and
so on, and you still pass them in `plugins: []`.
## "data-src is not provided on slide item" or the gallery opens empty
The selector does not match your markup. By default lightGallery treats the
direct children of the element you pass as the slides, and each slide needs
an `href` or `data-src` pointing at the full-size media. If the links sit
deeper in the markup, or the element holds other children, set
[`selector`](/docs/settings/#selector) to the links:
```js
lightGallery(document.getElementById('gallery'), {
selector: 'a',
});
```
The [attributes reference](/docs/attributes/) lists everything lightGallery
reads from a slide. In React, Vue and Angular the slides come from the
`slides` array or from trigger elements (`` in React,
`` in Vue, `[lgGalleryItem]` in Angular), so this warning means an
item without `src`.
## The slide fades in instead of zooming from its thumbnail
The item has no size. The zoom-from-origin animation needs the natural
size of the full image as `data-lg-size="WIDTH-HEIGHT"` (or `lgSize` on a
slide object in the framework packages), for example `1600-1067`. Without
it, or with a size that does not match the image, the gallery falls back to
a fade. Your image pipeline usually knows the dimensions; emit them into the
markup, as described in [getting started](/docs/getting-started/).
## Two galleries open, or clicks are handled twice
The same element is initialised twice. Each `lightGallery()` call creates a
new instance with its own click handlers, so a second call on the same
element opens two galleries. The usual causes are a script included twice,
an initialiser that runs on every render or route change, and hot module
reload during development.
- Keep the returned instance and reuse it. Call `refresh()` when the slides
change and `destroy()` before you really need a new instance.
- In React, Vue and Angular use the component and never call
`lightGallery()` yourself. The component owns the instance lifecycle,
including React Strict Mode's double-invoked effects.
## Slides added or removed after init do not show up
The gallery does not watch the DOM. After you add, remove or reorder the
slide elements, call [`refresh()`](/docs/methods/#refresh); in dynamic mode
pass the new array of items to `refresh(items)` or use
[`updateSlides(items, index)`](/docs/methods/#updateSlides) while the
gallery is open.
```js
const gallery = lightGallery(document.getElementById('gallery'));
// Later, after the markup changed:
gallery.refresh();
```
In React, Vue and Angular, rendering a different `slides` array (or
different trigger children) is the update; the handle also exposes
`refresh()`. See the [update slides demo](/demos/update-slides/).
## YouTube, Vimeo or HTML5 videos do not play
Check these in order:
1. The video plugin is registered (`lightgallery/plugins/video`, or the
`video` subpath of your framework package) and `lg-video.css` is
imported. Without the plugin a video URL is treated as an image and
fails to load.
2. Provider videos use the page URL in `href` (or `src`): a YouTube,
Vimeo or Wistia link, not an embed snippet. By default the slide shows a
poster and loads the player when the user presses play; see
[video facades](/docs/video-facades/) if you expect the player
immediately.
3. HTML5 videos take a JSON object in `data-video` (or the `video` field of
a slide object) with `source`, `attributes` and an optional `tracks`
list, and no `src`. The [video gallery demo](/demos/video-gallery/)
shows the exact shape for every package.
4. A console error that names videojs, player.js or Wistia means that
player script is not on the page. The video plugin controls those
players, it does not bundle them.
## The gallery appears behind a modal, header or cookie banner
Something on the page has a higher stacking order. The gallery overlay
(`.lg-outer`) sits at `z-index: 1050` and the backdrop just below it, which
is lower than some modal and toast libraries. Raise it in your stylesheet:
```css
.lg-backdrop,
.lg-outer {
z-index: 2000;
}
```
If you build the styles from `scss/`, set `$zindex-outer` and
`$zindex-backdrop` instead.
## Controls or the close button are missing on phones
That is the default on small screens. [`mobileSettings`](/docs/settings/#mobileSettings)
hides the arrows, the close icon and the download button below the desktop
breakpoint so gestures have the space. Turn them back on:
```js
lightGallery(document.getElementById('gallery'), {
mobileSettings: { controls: true, showCloseIcon: true, download: true },
});
```
## The console warns about the license key
Nothing is limited. The message tells you which case you are in:
- "please provide a valid license key": no key was set and the gallery is
running with no key at all. Pass [`licenseKey`](/docs/settings/#licenseKey)
or call `setLicenseKey()` once for the page.
- "license key is not valid for production use": you are using the
temporary key `0000-0000-000-0000`. It is meant for development and
evaluation; use your purchased key in production.
- "this license key is for v1 or v2 and is not valid for v3": lightGallery 3
keys start with `LIG`. See the [upgrade note](/license/).
The [license page](/docs/license/) explains which license applies to your
project. Open source projects under a GPLv3-compatible license can request
a key by email.
## Next.js: "Cannot read properties of null (reading 'removeChild')", "useRef is not a function" or hydration warnings
These come from running the gallery during server rendering or from the
retired React wrapper. With `@lightgallery/react`:
- Put the gallery in a client component (`'use client'` at the top of the
file). No `dynamic(() => …, { ssr: false })` wrapper is needed.
- Import the CSS in `app/layout.tsx` or another server component.
- Do not call the vanilla `lightGallery()` in a `useEffect` next to the
component; one or the other, never both.
```tsx
'use client';
import { LightGallery, LightGalleryItem } from '@lightgallery/react';
import Zoom from '@lightgallery/react/plugins/zoom';
export function Gallery() {
return (
);
}
```
The overlay never server-renders, in any of the packages, so Nuxt pages do
not need `` and Angular SSR has nothing to hydrate; see the
[React](/docs/react/#ssr--nextjs), [Vue](/docs/vue/#ssr--nuxt) and
[Angular](/docs/angular/#ssr--hydration) guides.
## Autoplay restarts from the first slide instead of stopping
That is the autoplay plugin's default: after the last slide it continues
from the first, whatever `loop` says. To stop at the end, watch the
[`lgAfterSlide`](/docs/events/#lgAfterSlide) event and stop the slideshow (or close the
gallery) when the last index is reached:
```js
import lightGallery from 'lightgallery';
import lgAutoplay from 'lightgallery/plugins/autoplay';
const el = document.getElementById('gallery');
const gallery = lightGallery(el, {
plugins: [lgAutoplay],
autoplay: { slideShowAutoplay: true, slideShowInterval: 3000 },
});
el.addEventListener('lgAfterSlide', (event) => {
if (event.detail.index === gallery.galleryItems.length - 1) {
// Plugin instances live on `gallery.plugins`.
gallery.plugins.find((p) => p.stopAutoPlay)?.stopAutoPlay();
}
});
```
The same event is `onAfterSlide` in React, `@after-slide` in Vue and
`(afterSlide)` in Angular; the slideshow also stops on the first user
interaction unless `forceSlideShowAutoplay` is set.
## I want custom HTML in a slide
Slides hold media: an image, a video or an iframe. For arbitrary content,
either render it as a page and open it as an
[iframe slide](/demos/iframe/) (`data-iframe="true"` with the URL in
`data-src`), or put the markup in the caption with `data-sub-html`, which
accepts HTML or a selector of an element on the page, as shown in the
[captions demo](/demos/captions/). The framework packages offer the
`caption` slot for the same purpose.
## Import errors after upgrading to version 3
`lightgallery/react`, `lightgallery/vue` and the Angular `LightgalleryModule`
were the version 2 wrappers and are gone. A bundler reports
`ERR_PACKAGE_PATH_NOT_EXPORTED` or "module not found" for those paths.
Install the native package for your stack and follow the
[migration guide](/docs/migration/), which maps every wrapper prop to its
version 3 equivalent:
```bash
npm install @lightgallery/react lightgallery
```
The vanilla `lightgallery` API itself did not change; only the framework
wrappers were replaced.
## TypeScript cannot find types for a plugin import
`lightgallery/plugins/zoom` and the other plugin subpaths resolve through
the package `exports` map, which TypeScript reads with
`"moduleResolution": "bundler"` (or `"node16"` / `"nodenext"`) in
`tsconfig.json`. The legacy `"node"` resolution ignores `exports` and
reports that the module has no declaration file. Switch the setting; the
code does not change.
## Still stuck?
Open a [Discussion](https://github.com/sachinchoolur/lightGallery/discussions)
with the package and version (`npm ls lightgallery @lightgallery/react`),
the console output and a StackBlitz reproduction. Bugs with a reproduction
go to the [issue tracker](https://github.com/sachinchoolur/lightGallery/issues).
If you work with a coding agent, the
[agent prompt](/docs/getting-started/#ask-your-coding-agent) points it at
the markdown version of these docs, this page included.
---
# Creating plugins
> How lightGallery plugins work and how to write your own: the plugin contract, settings, events, styles, and how a plugin is built and shipped.
Everything outside the core is a plugin: thumbnails, zoom, video, autoplay and the rest. The same contract is open to you.
Canonical page: https://www.lightgalleryjs.com/docs/creating-plugins/
This page covers plugins for the vanilla `lightgallery` package. The
native packages have their own plugin shapes that fit their frameworks:
React and Vue plugins are objects with render slots and a hook, and
Angular plugins are `withName()` feature functions. Each package exports
the types for writing one, `LgPlugin`, `LgPluginSlots` and
`PluginContext` in [React](/docs/react/), `LgVuePlugin`, `LgPluginSlots`
and `LgPluginContext` in [Vue](/docs/vue/), `LgFeature` and
`LgPluginContext` in [Angular](/docs/angular/), and its built-in plugins
are the reference implementations. Logic that has no DOM (math, state,
URL handling) belongs in [`@lightgallery/headless`](/docs/headless/) so
every package can share it.
## The contract
A plugin is a class. lightGallery constructs it with the gallery instance
and its DOM utility, then calls a few well-known methods at the right
moments:
| Method | When it runs |
| --- | --- |
| `constructor(instance, $LG)` | Once, while the gallery is being set up. Store the instance and merge your defaults with the user's settings. Do not touch the DOM yet. |
| `init()` | After the gallery structure exists and every plugin is constructed. Add your UI, subscribe to events. |
| `closeGallery()` | Optional. Each time the gallery closes. Pause timers, reset state. |
| `destroy()` | When the gallery is destroyed. Remove your UI and listeners. |
Pass the class in the `plugins` setting and lightGallery does the rest:
```js
lightGallery(document.getElementById('gallery'), {
plugins: [lgZoom, lgThumbnail, MyPlugin],
});
```
## A complete plugin
A small but real one: a toolbar button that shows the slide position and
keeps it current as the user navigates.
```ts
// lg-counter.ts
import { lGEvents } from '../../lg-events';
import { LgQuery } from '../../lgQuery';
import { LightGallery } from '../../lightgallery';
import { CounterSettings, counterSettings } from './lg-counter-settings';
export default class Counter {
core: LightGallery;
settings: CounterSettings;
private $LG: LgQuery;
constructor(instance: LightGallery, $LG: LgQuery) {
this.core = instance;
this.$LG = $LG;
// Plugin defaults first, then whatever the user passed.
this.settings = { ...counterSettings, ...this.core.settings };
}
init(): void {
if (!this.settings.counter) {
return;
}
this.core.$toolbar.append(
``,
);
// Namespace your listeners so `destroy()` can remove only yours.
this.core.LGel.on(`${lGEvents.afterSlide}.counter`, () => {
this.core.outer.find('.lg-counter-btn').html(this.label());
});
}
label(): string {
const template = this.settings.counterTemplate;
return template
.replace('{index}', String(this.core.index + 1))
.replace('{total}', String(this.core.galleryItems.length));
}
closeGallery(): void {
// Nothing to pause here; this is where a timer plugin would stop.
}
destroy(): void {
this.core.LGel.off('.counter');
this.core.outer.find('.lg-counter-btn').remove();
}
}
```
The settings live in their own file, typed and with defaults:
```ts
// lg-counter-settings.ts
export interface CounterSettings {
/** Enable the counter plugin. */
counter: boolean;
/** `{index}` and `{total}` are replaced with the slide position. */
counterTemplate: string;
}
export const counterSettings: CounterSettings = {
counter: true,
counterTemplate: '{index} of {total}',
};
```
What the plugin has access to:
- **`this.core`**, the gallery instance: `index`, `galleryItems`,
`settings`, `outer` (the `.lg-outer` element), `$toolbar`,
`$content`, and every public [method](/docs/methods/).
- **`this.core.LGel`**, the element the gallery was initialized on. All
[events](/docs/events/) fire on it; subscribe with a namespace
(`${lGEvents.afterSlide}.counter`) so you can `off('.counter')` later.
- **`this.$LG`**, a tiny DOM utility (`find`, `on`, `off`, `addClass`,
`html`, `css`, …) so plugins need no dependencies.
## Settings, strings and icons
- Merge your defaults under the user's settings as in the example;
lightGallery does not merge plugin settings for you.
- Labels that users may want to translate belong in `strings`. Read them
from `this.core.settings.strings` so one localized object covers the
core and every plugin.
- The built-in plugins register their SVG artwork with
`this.core.registerDefaultIcons(...)` so the [`icons`](/docs/custom-icons/)
setting can replace it by name. A plugin outside this repository draws
its own buttons; put the SVG in your markup and keep it
`currentColor`-friendly so it follows the toolbar's colors.
## Styles
Plugin styles are separate files, so consumers pay only for what they
use. In this repository a plugin's stylesheet is `src/scss/lg-.scss`
and ships as `lightgallery/css/lg-.css`. Outside the repository,
ship a CSS file next to your plugin and document the import the same way.
## Using a plugin outside this repository
You do not need lightGallery's build system. Write the class in your own
project, type it against the package, and pass it in `plugins`:
```ts
import type { LightGallery } from 'lightgallery';
export default class Ping {
core: LightGallery;
constructor(instance: LightGallery) {
this.core = instance;
}
init(): void {
console.log('gallery ready', this.core.galleryItems.length);
}
destroy(): void {}
}
```
## Adding a plugin to this repository
If you are contributing a plugin to lightGallery itself, the build picks
it up from one registry:
1. Create `src/plugins//lg-.ts` and
`src/plugins//lg--settings.ts` (and `src/scss/lg-.scss`
if it needs styles).
2. Register the entry in `plugins-config-rollup.json`:
```json
{
"name": "counter",
"folder": "plugins/counter/",
"fileName": "lg-counter"
}
```
3. Add the settings type to `LightGalleryAllSettings` and `MobileSettings`
in `src/lg-settings.ts`, so `plugins: [...]` users get typed settings.
4. Add the package stub `packages/plugins//package.json` (copy an
existing one) and the `./plugins/` entry in the root
`package.json` `exports` map. Both point at the files the build emits.
5. Try it in the vanilla dev rig: import it in `dev-vanilla/main.ts` and run
`npx vite --config dev-vanilla/vite.config.ts`.
6. `npm run build` produces `dist/plugins//` with ES, UMD and
minified bundles plus types; `npm test` runs the suites in `test/`.
Then mirror it: a plugin in the vanilla package should exist in the React,
Vue and Angular packages too, with the same settings and behavior.
---
# @lightgallery/headless
> Framework-free lightGallery core: state machine, gesture math and plugin logic shared by every v3 binding.
Framework-free lightGallery core: state machine, gesture math and plugin logic shared by every v3 binding.
Canonical page: https://www.lightgalleryjs.com/docs/headless/
`@lightgallery/headless` is the framework-free core of lightGallery 3:
everything a gallery needs to decide, with none of the rendering. It holds
the state machine, settings resolution, gesture verdicts, spring physics,
zoom and thumbnail math, the justified layout, responsive image selection,
video URL builders, share links and URL drivers. It has no DOM and no
framework; its TypeScript config excludes the DOM library, so `window`
and `document` do not even type-check inside it.
The vanilla `lightgallery` package, [`@lightgallery/react`](/docs/react/),
[`@lightgallery/vue`](/docs/vue/) and
[`@lightgallery/angular`](/docs/angular/) are all renderers over this
core. That is why a swipe threshold, a zoom clamp or a setting behaves
the same in every package: it is the same code.
## Who it is for
If you use one of the four packages you never install this one; each
depends on it. Install it directly to build a binding for another
framework, a web component, or a custom gallery UI on the same
foundation.
```bash
npm install @lightgallery/headless
```
## What it exports
| Area | Exports | Notes |
| --- | --- | --- |
| State | `createGalleryState`, `galleryReducer`, `clampIndex` | Pure reducer over `OPEN`, `CLOSE`, `NEXT`, `PREV`, `GO_TO` and the load and transition actions |
| Settings | `resolveSettings`, `coreSettingsDefaults` | Merges user settings, mobile settings and `strings` over the defaults |
| Items | `getSlideType` | Image, video, iframe or inline, from an item's fields |
| Slide windowing | `getSlideIndexesInDom`, `getPreloadIndexes`, `getSlidePoolIndexes` | Which slides to mount and preload, including virtualization |
| Gestures | `getSwipeAxis`, `getSwipeReleaseVerdict`, `getHorizontalDragTransforms`, `getVerticalDragEffects`, `shouldCloseOnVerticalDrag`, `getEdgeFrictionedDelta`, `upsertPointer`, `removePointer` | Axis lock, flick detection, boundary friction, drag to close |
| Physics | `stepSpring`, `isSpringSettled`, `project`, `getWindowedVelocity`, `pushVelocitySample` | Damped spring, momentum projection, windowed release velocity |
| Zoom | `getPinchScale`, `getPinchPan`, `getPointZoomPan`, `clampScale`, `clampPan`, `getPanBounds`, `shouldCloseOnPinch`, `getActualSizeScale` | Focal-point pinch, pan bounds with friction, pinch to close |
| Thumbnails | `getThumbWindow`, `getThumbCorridorWindow`, `getActiveThumbTranslate`, `clampThumbTranslate`, `getElasticThumbTranslate`, `getScrubThumbIndex`, `getScrubThumbTranslate` | Strip windowing, centering, elastic edges, scrubbing |
| Layout | `getJustifiedLayout`, `getJustifiedRows` | Row-justified box layout for trigger grids |
| Responsive | `parseSrcset`, `resolveImageSource`, `resolveSizes`, `matchesMedia`, `awaitDecode` | `srcset` and `sizes` selection, decode gating |
| Origin animation | `getOriginTransform`, `fitImageSize`, `parseImageSize` | Open-from-trigger transforms |
| Origin crop | `getOriginWindow`, `getOriginCropFlight`, `parseObjectFit`, `parseBackgroundFit`, `parseCssPosition` | The origin crop plugin's flight from a cropped thumbnail |
| Toolbar | `getToolbarOverflow`, `getToolbarItemPriority` | Which buttons move into the overflow menu |
| Video | `getVideoInfo`, `getYouTubeEmbedUrl`, `getVimeoEmbedUrl`, `getWistiaEmbedUrl`, `getYouTubePosterUrl`, `getFacadePoster` | Provider detection, embed URLs, facade posters |
| Share | `getSharePayload`, `canNativeShare`, `getXShareLink`, `getFacebookShareLink`, `getPinterestShareLink` | Web Share payload and social fallbacks |
| URL drivers | `createHashDriver`, `createHistoryHashDriver`, `createNavigationHashDriver` | One interface over hash, history and the Navigation API |
| Plugin slices | `initialZoomSlice`, `applyZoom`, `initialRotateSlice`, `rotateLeft`, `rotateRight`, `flipHorizontal`, `flipVertical`, `initialAutoplaySlice` | Per-plugin state reducers |
| Accessibility | `formatSlideAnnouncement` | Live-region text from a template string |
| Icons | `coreDefaultIcons`, `zoomDefaultIcons`, …, `LgIconName` | The built-in inline SVG icon sets, grouped by feature |
| License | `setLicenseKey`, `checkLicenseKey` | The license check every package shares |
| Events | `createEmitter` | A typed emitter for renderers |
Each module is independent and the package has no side effects; import
only what your renderer needs.
## Example
```ts
import {
createGalleryState,
galleryReducer,
getSlideIndexesInDom,
resolveSettings,
} from '@lightgallery/headless';
const settings = resolveSettings({ loop: true, speed: 400 });
let state = createGalleryState({ slidesCount: 5, loop: settings.loop });
state = galleryReducer(state, { type: 'OPEN', index: 2 });
state = galleryReducer(state, { type: 'NEXT' });
// Which slides to keep mounted right now: the current slide, its
// neighbours, and the previous slide so an outgoing transition can finish.
const mounted = getSlideIndexesInDom(
state.currentIndex,
state.previousIndex,
state.slidesCount,
settings.numberOfSlideItemsInDom,
settings.loop,
);
```
Every function is pure. A renderer wraps the reducer in its own
reactivity, a reducer hook in React, a store in Vue, a signal in Angular,
and owns every DOM concern: measuring, painting, pointer events and
focus.
## Related
- [Creating plugins](/docs/creating-plugins/) for the plugin surface
the renderers expose.
- The [React](/docs/react/), [Vue](/docs/vue/) and
[Angular](/docs/angular/) guides, each a renderer over this package.
## License
Free and open source under the GPLv3, like every lightGallery package. If
your project keeps its source proprietary, a [commercial license](/license/)
covers it: same code, nothing gated.
---
# @lightgallery/react
> A native React lightGallery component. React renders the triggers and the lightbox, with no wrapper and no second runtime touching your DOM.
A native React lightGallery component. React renders the triggers and the lightbox, with no wrapper and no second runtime touching your DOM.
Canonical page: https://www.lightgalleryjs.com/docs/react/
`@lightgallery/react` is a native React component, not a wrapper around
the vanilla script. React renders every node, in the trigger grid and in
the lightbox, so nothing else mutates your DOM and the gallery behaves
like the rest of your tree: props in, callbacks out. Styling reuses the
published `lightgallery/css/*` files, so the lightbox looks exactly like
the vanilla one.
## Install & styles
```bash
npm install @lightgallery/react lightgallery
```
Peer range: React 18 or 19 (`react` and `react-dom`).
```tsx
// CSS is a consumer import, the React package ships no CSS.
import 'lightgallery/css/lightgallery.css';
// Per-plugin styles as needed:
import 'lightgallery/css/lg-thumbnail.css';
import 'lightgallery/css/lg-zoom.css';
```
## Uncontrolled (thumbnail grid)
```tsx
import { LightGallery, LightGalleryItem } from '@lightgallery/react';
import Thumbnail from '@lightgallery/react/plugins/thumbnail';
import Zoom from '@lightgallery/react/plugins/zoom';
const items = [
{
src: 'img/1-1600.jpg',
thumb: 'img/1-240.jpg',
alt: 'Mountains',
lgSize: '1600-1067', // natural size, enables the zoom-from-origin open animation
caption:
Mountains
,
},
];
export function Gallery() {
return (
{items.map((item) => (
))}
);
}
```
## Controlled
Pass `slides` instead of children and the component renders no triggers;
you own `open` and `index` and open the gallery from anything you like.
```tsx
import { useState } from 'react';
import { LightGallery } from '@lightgallery/react';
const items = [
{ src: 'img/1-1600.jpg', thumb: 'img/1-240.jpg', alt: 'Mountains' },
{ src: 'img/2-1600.jpg', thumb: 'img/2-240.jpg', alt: 'Forest' },
];
export function Gallery() {
const [open, setOpen] = useState(false);
const [index, setIndex] = useState(0);
return (
<>
setOpen(false)}
index={index}
onIndexChange={setIndex}
/>
>
);
}
```
## Imperative
A `ref` exposes `openGallery(index?)`, `closeGallery()`,
`goToSlide(index)`, `nextSlide()`, `prevSlide()` and `refresh()`.
```tsx
import { useRef } from 'react';
import { LightGallery, type LightGalleryRefHandle } from '@lightgallery/react';
const items = [
{ src: 'img/1-1600.jpg', thumb: 'img/1-240.jpg', alt: 'Mountains' },
{ src: 'img/2-1600.jpg', thumb: 'img/2-240.jpg', alt: 'Forest' },
];
export function Gallery() {
const gallery = useRef(null);
return (
<>
>
);
}
```
## Settings, events and render slots
Settings are flat props with the vanilla names (`mode`, `speed`, `loop`,
`captionPosition`, …); the [settings reference](/docs/settings/) lists
every one. Per-plugin settings are a prop named after the plugin
(`zoom={{ scale: 1.5 }}`), typed via module augmentation from each plugin
entry. Lifecycle callbacks use the documented [event names](/docs/events/)
with an `on` prefix (`onBeforeSlide`, `onAfterSlide`, `onSlideItemLoad`,
…).
The `render` prop swaps parts of the chrome for your own components:
`caption(item, index)`, `counter(current, total)`, `prevButton()`,
`nextButton()` and `icon(name)`, which replaces a
[control icon](/docs/custom-icons/) by name. An inline gallery mounts into
an element you pass as `container`.
## SSR / Next.js
The gallery is SSR-safe by construction: every entry point imports cleanly
in bare Node (ESM and CJS), and `renderToString` emits nothing for the
gallery itself, the portal mounts client-side after hydration. Trigger
children (``) server-render as static markup, so grids
are crawlable.
With the Next.js App Router the component must live in a client component:
```tsx
'use client';
import { LightGallery, LightGalleryItem } from '@lightgallery/react';
// ... as above
```
Import the CSS in your root layout (or any server component):
```tsx
// app/layout.tsx
import 'lightgallery/css/lightgallery.css';
```
No `dynamic(() => …, { ssr: false })` wrapper is needed. The
[Next.js image gallery demo](/demos/nextjs-image-gallery/) walks through
an App Router page with `next/image` thumbnails.
## Accessibility
The open gallery is a modal dialog (`role="dialog"`, `aria-modal`, an
accessible name). Focus moves into the gallery on open, Tab is trapped
while it is open and focus returns to the trigger on close. Thumbnails and
pager dots are keyboard-operable, and `prefers-reduced-motion` disables
the animations. The open gallery passes axe WCAG A/AA checks in CI. The
[accessibility page](/docs/accessibility/) covers the live region, the
labels and the settings involved.
## Keyboard bindings
| Key | Action |
|---|---|
| `Esc` | close (`escKey`) |
| `←` / `→` | previous / next slide (`keyPress`) |
| `Tab` / `Shift+Tab` | cycle focus within the gallery (`trapFocus`) |
| `Enter` / `Space` | activate focused thumbnail / pager dot |
## Plugins
All 14 plugins ship as subpath imports, plus the justified layout; pass them via `plugins={[]}`
and configure each with the prop named after it. Import the matching
`lightgallery/css/lg-*.css` where one exists.
| Plugin | Import | Key options (prop of the same name) |
|---|---|---|
| Thumbnail | `@lightgallery/react/plugins/thumbnail` | `thumbWidth`, `thumbHeight`, `thumbMargin`, `animateThumb`, `toggleThumb` |
| Zoom | `@lightgallery/react/plugins/zoom` | `scale`, `actualSize`, `showZoomInOutIcons`, `infiniteZoom`, `enableZoomAfter` |
| Video | `@lightgallery/react/plugins/video` | `autoplayFirstVideo`, `autoplayVideoOnSlide`, `youTubePlayerParams`, `vimeoPlayerParams`, `gotoNextSlideOnVideoEnd` |
| Autoplay | `@lightgallery/react/plugins/autoplay` | `slideShowAutoplay`, `slideShowInterval`, `progressBar`, `forceSlideShowAutoplay` |
| Fullscreen | `@lightgallery/react/plugins/fullscreen` | `fullScreen` |
| Hash | `@lightgallery/react/plugins/hash` | `galleryId`, `customSlideName` |
| Pager | `@lightgallery/react/plugins/pager` | `pager` |
| Share | `@lightgallery/react/plugins/share` | `facebook`, `twitter`, `pinterest`, `additionalShareOptions` (typed objects) |
| Rotate | `@lightgallery/react/plugins/rotate` | `rotateSpeed`, `rotateLeft/Right`, `flipHorizontal/Vertical` |
| Comment | `@lightgallery/react/plugins/comment` | `commentBox`, `renderComments(item)` render prop |
| MediumZoom | `@lightgallery/react/plugins/mediumZoom` | `margin`, `backgroundColor` (+ per-item `lgBackgroundColor`) |
| RelativeCaption | `@lightgallery/react/plugins/relativeCaption` | `relativeCaption` |
| VimeoThumbnail | `@lightgallery/react/plugins/vimeoThumbnail` | `showVimeoThumbnails`, `showThumbnailWithPlayButton` |
| OriginCrop | `@lightgallery/react/plugins/originCrop` | `originCrop`: flies a cropped thumbnail from its crop ([zoom from origin](/demos/zoom-from-origin/)) |
| Justified | `@lightgallery/react/plugins/justified` | Not a plugin: the `` component wraps the triggers, with `rowHeight`, `gap`, `lastRow` ([justified layout](/docs/justified-layout/)) |
Order matters for slide wrappers: put `Zoom` before `Rotate` so zoom stays
the outermost transform.
## Migrating from the 2.x React wrapper
Coming from `lightgallery/react`, the wrapper that shipped inside the
vanilla 2.x package? The full list is in the
[migration guide](/docs/migration/#react); the key changes:
- Items are data: `dynamic`/`dynamicEl` and every DOM-scraping option
(`selector`, `extraProps`, `exThumbImage`, …) are gone, pass `slides`
or wrap thumbnails in ``.
- HTML-string options became typed render props: `subHtml` →
`item.caption` (ReactNode) or `item.captionHtml` (explicit raw HTML);
`nextHtml`/`prevHtml`/`appendCounterTo` → the `render` prop slots.
- `appendSubHtmlTo` → `captionPosition: 'bar' | 'slide' | 'outer'`;
`addClass` → `className`; `index` → controlled `index`/`onIndexChange`
or `defaultIndex`.
- Plugins are modules, not constructors: `plugins={[Zoom]}` with settings
as a `zoom={{ … }}` prop instead of flat settings keys.
- Events keep their documented `onXxx` names and payloads; `updateSlides`
is gone (changing `slides` is the update). The `videojs` option was
dropped, bring custom players through a plugin `slideRenderer`.
## Next steps
- [React image gallery](/demos/react-image-gallery/),
[video gallery](/demos/react-video-gallery/),
[carousel](/demos/react-carousel/) and
[video carousel](/demos/react-video-carousel/) demos, each with the
code behind it.
- Features that work the same in every package:
[justified layout](/docs/justified-layout/),
[virtualization](/docs/virtualization/),
[thumbnail scrubbing](/docs/thumbnail-scrubbing/),
[video facades](/docs/video-facades/),
[custom icons](/docs/custom-icons/),
[localization and RTL](/docs/localization-rtl/) and
[responsive loading](/docs/responsive-loading/).
- The [settings reference](/docs/settings/), every setting is a prop of
the same name, and the [events](/docs/events/) list, each one an
`on*` callback here.
## License
Free and open source under the GPLv3, like every lightGallery package. If
your project keeps its source proprietary, a [commercial license](/license/)
covers it: same code, nothing gated. Use `0000-0000-000-0000` as a temporary
`licenseKey` for evaluation.
---
# @lightgallery/vue
> A native Vue 3 lightGallery component. Vue renders the triggers and the lightbox, with v-model, typed emits, scoped slots and a subpath per plugin.
A native Vue 3 lightGallery component. Vue renders the triggers and the lightbox, with v-model, typed emits, scoped slots and a subpath per plugin.
Canonical page: https://www.lightgalleryjs.com/docs/vue/
`@lightgallery/vue` is a native Vue 3 component, not a wrapper around the
vanilla script. Vue renders every node, in the trigger grid and in the
lightbox, so nothing else mutates your DOM. You get `
```
## Controlled
Pass `:slides` instead of `` children and the component renders no
triggers; `v-model:open` and `v-model:index` give you the state.
```vue
```
## Imperative
A template ref exposes `openGallery(index?)`, `closeGallery()`,
`goToSlide(index)`, `nextSlide()`, `prevSlide()` and `refresh()`.
```vue
```
## Settings, events and slots
Settings are same-named props (`:mode`, `:speed`, `:loop`,
`:caption-position`, …); the [settings reference](/docs/settings/) lists
every one. Events are kebab-case emits of the documented
[event names](/docs/events/) without the `on` prefix (`@before-open`,
`@after-slide`, `@slide-item-load`, …).
Scoped slots swap parts of the chrome for your own markup: `#caption`
(`{ item, index }`), `#counter` (`{ current, total }`), `#prev-button`,
`#next-button` and, with the comment plugin, `#comments` (`{ item, index }`).
The `:icons` prop replaces any [control icon](/docs/custom-icons/) by
name. An inline gallery mounts into the element you pass as `:container`.
## Plugins (all 14, plus the justified layout)
Each plugin is its own tree-shakable subpath
`@lightgallery/vue/plugins/` exporting a plugin object for the
`:plugins` prop. Per-plugin settings go on a same-named gallery prop
(e.g. `:zoom="{ scale: 1.5 }"`). A multi-word plugin name works in either
spelling, `:medium-zoom="{ margin: 24 }"` or `:mediumZoom`:
| Plugin | Subpath | Key options (prop of the same name) |
|---|---|---|
| thumbnail | `plugins/thumbnail` | `thumbWidth`, `thumbHeight`, `thumbMargin`, `animateThumb`, `toggleThumb` |
| zoom | `plugins/zoom` | `scale`, `actualSize`, `showZoomInOutIcons`, `infiniteZoom`, `enableZoomAfter` |
| video | `plugins/video` | `autoplayFirstVideo`, `autoplayVideoOnSlide`, `youTubePlayerParams`, `vimeoPlayerParams`, `gotoNextSlideOnVideoEnd` |
| autoplay | `plugins/autoplay` | `slideShowAutoplay`, `slideShowInterval`, `progressBar`, `forceSlideShowAutoplay` |
| fullscreen | `plugins/fullscreen` | `fullScreen` |
| hash | `plugins/hash` | `galleryId`, `customSlideName` |
| pager | `plugins/pager` | `pager` |
| share | `plugins/share` | `facebook`, `twitter`, `pinterest`, `additionalShareOptions` (typed objects) |
| rotate | `plugins/rotate` | `rotateSpeed`, `rotateLeft/Right`, `flipHorizontal/Vertical` |
| comment | `plugins/comment` | `commentBox`; the comment body comes from the `#comments` slot |
| mediumZoom | `plugins/mediumZoom` | `margin`, `backgroundColor` (+ per-item `lgBackgroundColor`) |
| relativeCaption | `plugins/relativeCaption` | `relativeCaption` (presets `captionPosition: 'slide'`) |
| vimeoThumbnail | `plugins/vimeoThumbnail` | `showVimeoThumbnails`, `showThumbnailWithPlayButton` |
| originCrop | `plugins/originCrop` | `originCrop`: flies a cropped thumbnail from its crop ([zoom from origin](/demos/zoom-from-origin/)) |
| justified | `plugins/justified` | Not a plugin: the `` component wraps the triggers, with `row-height`, `gap`, `last-row` ([justified layout](/docs/justified-layout/)) |
Plugins compose per gallery instance, two galleries on one page can have
different plugin sets. Order matters for slide wrappers: put `Zoom` before
`Rotate` so zoom stays the outermost transform.
## SSR / Nuxt
- Server-safe: every entry imports without browser globals, and the closed
gallery server-renders only your trigger markup. The lightbox overlay
**never server-renders** (even with `open` true at first render), the
`` mounts client-side only, so there is no teleport buffer to
wire up and no hydration mismatch surface.
- In Nuxt, use the component directly in server-rendered pages, no
`` wrapper needed. Deep-link flows (hash plugin) run after
hydration.
- Import the CSS globally (`nuxt.config` `css: ['lightgallery/css/...']`).
## Accessibility
The open gallery is a modal dialog (`role="dialog"`, `aria-modal`, an
accessible name, with an `:aria-labelledby` override). Focus moves in on
open, Tab and Shift+Tab are trapped while it is open and focus returns to
the trigger on close. Every button is labelled, thumbnails and pager dots
are keyboard-operable, and `prefers-reduced-motion` disables the
animations. The open gallery passes axe WCAG A/AA checks in CI. The
[accessibility page](/docs/accessibility/) covers the live region, the
labels and the settings involved.
## Migrating from the legacy `lightgallery/vue` wrapper
Coming from `lightgallery/vue`, the wrapper that shipped inside the
vanilla 2.x package? The full list is in the
[migration guide](/docs/migration/#vue); the key changes:
- `dynamicEl` → `:slides` (typed `LgGalleryItem[]`), or `` trigger
components for uncontrolled galleries.
- `onAfterSlide` etc. → kebab-case emits without the prefix:
`@after-slide`.
- `appendSubHtmlTo` → `captionPosition: 'bar' | 'slide' | 'outer'`;
`subHtml` strings → `caption` (plain string), the `#caption` slot, or the
explicit raw-HTML `captionHtml` opt-in.
- Plugin constructor arrays → plugin objects on `:plugins`, options via
same-named gallery props.
- Dropped (2.x DOM-scraping/HTML-string era): `selector`, `extraProps`,
`getCaptionFromTitleOrAlt`, `nextHtml`/`prevHtml`, `appendCounterTo`,
`videojs`.
## Next steps
- [Vue image gallery](/demos/vue-image-gallery/) and
[video gallery](/demos/vue-video-gallery/) demos, each with the code
behind it.
- Features that work the same in every package:
[justified layout](/docs/justified-layout/),
[virtualization](/docs/virtualization/),
[thumbnail scrubbing](/docs/thumbnail-scrubbing/),
[video facades](/docs/video-facades/),
[custom icons](/docs/custom-icons/),
[localization and RTL](/docs/localization-rtl/) and
[responsive loading](/docs/responsive-loading/).
- The [settings reference](/docs/settings/), every setting is a prop of
the same name, and the [events](/docs/events/) list, each one a
kebab-case emit here.
## License
Free and open source under the GPLv3, like every lightGallery package. If
your project keeps its source proprietary, a [commercial license](/license/)
covers it: same code, nothing gated. Use `0000-0000-000-0000` as a temporary
`licenseKey` for evaluation.
---
# @lightgallery/angular
> A native Angular lightGallery component. Angular renders the triggers and the lightbox, with standalone components, signal inputs and outputs and zoneless change detection.
A native Angular lightGallery component. Angular renders the triggers and the lightbox, with standalone components, signal inputs and outputs and zoneless change detection.
Canonical page: https://www.lightgalleryjs.com/docs/angular/
`@lightgallery/angular` is a native Angular component, not a wrapper
around the vanilla script. Angular renders every node, in the trigger grid
and in the lightbox, so nothing else mutates your DOM. You get standalone
components, signal inputs and outputs, zoneless change detection, CDK
overlay and a11y, and a secondary entry point per plugin in Angular
Package Format. Styling reuses the published `lightgallery/css/*` files,
so the lightbox looks exactly like the vanilla one.
## Install
```bash
npm install @lightgallery/angular @angular/cdk lightgallery
```
Peer ranges: `@angular/core`, `@angular/common`, `@angular/platform-browser`
and `@angular/cdk` `>=21 <23`. There is no `zone.js` in the package.
The stylesheets ship from the `lightgallery` package. Import them in your
root stylesheet, or add the same paths to the `styles` array in
`angular.json`:
```css
/* src/styles.css */
@import 'lightgallery/css/lightgallery.css';
/* plus the CSS of each feature you use, e.g.: */
@import 'lightgallery/css/lg-thumbnail.css';
@import 'lightgallery/css/lg-zoom.css';
```
## Quick start, uncontrolled
Thumbnails on the page open the lightbox; mount order defines slide order.
```ts
import { Component } from '@angular/core';
import {
LgGalleryComponent,
LgGalleryItemDirective,
type LgGalleryItem,
type SlideEventDetail,
} from '@lightgallery/angular';
import { withThumbnail } from '@lightgallery/angular/plugins/thumbnail';
import { withZoom } from '@lightgallery/angular/plugins/zoom';
@Component({
imports: [LgGalleryComponent, LgGalleryItemDirective],
template: `
@for (item of items; track item.src) {
}
`,
})
export class Gallery {
features = [withThumbnail({ thumbWidth: 120 }), withZoom()];
items: LgGalleryItem[] = [
{
src: 'img/1.jpg',
thumb: 'img/1-t.jpg',
alt: 'Mountains',
caption: 'Mountains',
lgSize: '1600-1067', // natural size, enables the zoom-from-origin open animation
},
];
onSlide(event: SlideEventDetail): void {
console.log(event.index, event.prevIndex);
}
}
```
## Controlled
Pass `[slides]` instead of `[lgGalleryItem]` children and the component
renders no triggers; `[open]` with `(closed)` and the two-way `[(index)]`
give you the state.
```ts
import { Component, signal } from '@angular/core';
import { LgGalleryComponent, type LgGalleryItem } from '@lightgallery/angular';
@Component({
imports: [LgGalleryComponent],
template: `
`,
})
export class Gallery {
open = signal(false);
index = signal(0);
items: LgGalleryItem[] = [
{ src: 'img/1.jpg', thumb: 'img/1-t.jpg', alt: 'Mountains' },
{ src: 'img/2.jpg', thumb: 'img/2-t.jpg', alt: 'Forest' },
];
}
```
## Imperative
A template reference (`#lg="lgGallery"`) exposes `openGallery(index?)`,
`closeGallery()`, `goToSlide(index)`, `nextSlide()`, `prevSlide()` and
`refresh()`.
```ts
import { Component } from '@angular/core';
import { LgGalleryComponent, type LgGalleryItem } from '@lightgallery/angular';
@Component({
imports: [LgGalleryComponent],
template: `
`,
})
export class Gallery {
items: LgGalleryItem[] = [
{ src: 'img/1.jpg', thumb: 'img/1-t.jpg', alt: 'Mountains' },
{ src: 'img/2.jpg', thumb: 'img/2-t.jpg', alt: 'Forest' },
];
}
```
## Settings, events and slots
Settings are same-named signal inputs (`[mode]`, `[speed]`, `[loop]`,
`[captionPosition]`, …); the [settings reference](/docs/settings/) lists
every one. Events are outputs of the documented
[event names](/docs/events/) without the `on` prefix (`(beforeOpen)`,
`(afterSlide)`, `(slideItemLoad)`, …).
Templates swap parts of the chrome for your own markup. Each is an
`ng-template` carrying one of the directives `lgCaption`
(`let-item let-index="index"`), `lgCounter` (`let-current let-total="total"`),
`lgPrevButton` or `lgNextButton`, placed inside ``:
```html
{{ item?.caption }}
Slide {{ index + 1 }}
```
An `lgIcon` template replaces any [control icon](/docs/custom-icons/) by
name. An inline gallery mounts into the element you pass as `[container]`.
## Features (all 14, plus the justified layout)
Each feature is its own tree-shakable entry point
`@lightgallery/angular/plugins/` exposing a `with(options?)`
factory for the `[features]` input:
| Feature | Import | Key options (passed to the factory) |
|---|---|---|
| thumbnail | `withThumbnail()` | `thumbWidth`, `thumbHeight`, `thumbMargin`, `animateThumb`, `toggleThumb` |
| zoom | `withZoom()` | `scale`, `actualSize`, `showZoomInOutIcons`, `infiniteZoom`, `enableZoomAfter` |
| video | `withVideo()` | `autoplayFirstVideo`, `autoplayVideoOnSlide`, `youTubePlayerParams`, `vimeoPlayerParams`, `gotoNextSlideOnVideoEnd` |
| autoplay | `withAutoplay()` | `slideShowAutoplay`, `slideShowInterval`, `progressBar`, `forceSlideShowAutoplay` |
| fullscreen | `withFullscreen()` | `fullScreen` |
| hash | `withHash()` | `galleryId`, `customSlideName` |
| pager | `withPager()` | `pager` |
| share | `withShare()` | `facebook`, `twitter`, `pinterest`, `additionalShareOptions` (typed objects) |
| rotate | `withRotate()` | `rotateSpeed`, `rotateLeft/Right`, `flipHorizontal/Vertical` |
| comment | `withComment()` | `commentBox`, `commentsTemplate: TemplateRef` |
| mediumZoom | `withMediumZoom()` | `margin`, `backgroundColor` (+ per-item `lgBackgroundColor`) |
| relativeCaption | `withRelativeCaption()` | `relativeCaption` (presets `captionPosition: 'slide'`) |
| vimeoThumbnail | `withVimeoThumbnail()` | `showVimeoThumbnails`, `showThumbnailWithPlayButton` |
| originCrop | `withOriginCrop()` | `originCrop`: flies a cropped thumbnail from its crop ([zoom from origin](/demos/zoom-from-origin/)) |
| justified | `LgJustifiedGridComponent` | Not a feature: the `` component wraps the triggers, with `rowHeight`, `gap`, `lastRow` ([justified layout](/docs/justified-layout/)) |
Features compose per gallery instance, two galleries on one page can have
different feature sets. Order matters for slide wrappers: put `withZoom()`
before `withRotate()` so zoom stays the outermost transform.
## SSR / hydration
- Server-safe and zoneless: with `@angular/ssr` the page server-renders
your trigger markup as static HTML; the lightbox overlay **never
server-renders** (even with `[open]` true at bootstrap), so there is
nothing to hydrate-mismatch. The overlay is created on open, in the
browser only.
- Deep links (hash feature) resolve after hydration, in the browser only.
## Accessibility
The open gallery is a modal dialog (`role="dialog"`, `aria-modal`, an
accessible name). The CDK focus trap moves focus in on open, keeps Tab
inside while it is open and returns focus to the trigger on close. Every
button is labelled, thumbnails and pager dots are keyboard-operable, and
`prefers-reduced-motion` disables the animations. The open gallery passes
axe WCAG A/AA checks in CI. The [accessibility page](/docs/accessibility/)
covers the live region, the labels and the settings involved.
## Migrating from the legacy `lightgallery` Angular wrapper
Coming from `lightgallery/angular`, the wrapper that shipped inside the
vanilla 2.x package? The full list is in the
[migration guide](/docs/migration/#angular); the key changes:
- `dynamicEl` → `[slides]` (typed `LgGalleryItem[]`), or `[lgGalleryItem]`
trigger directives for uncontrolled galleries.
- `onAfterSlide` etc. → outputs without the prefix: `(afterSlide)`.
- `appendSubHtmlTo` → `captionPosition: 'bar' | 'slide' | 'outer'`;
`subHtml` strings → `caption` (plain string), an `lgCaption` template, or
the explicit raw-HTML `captionHtml` opt-in (Angular-sanitized).
- Plugin constructor arrays → `with*()` feature values on `[features]`.
- Dropped (2.x DOM-scraping/HTML-string era): `selector`, `extraProps`,
`getCaptionFromTitleOrAlt`, `nextHtml`/`prevHtml`, `appendCounterTo`,
`videojs`.
## Next steps
- [Angular image gallery](/demos/angular-image-gallery/) and
[video gallery](/demos/angular-video-gallery/) demos, each with the
code behind it.
- Features that work the same in every package:
[justified layout](/docs/justified-layout/),
[virtualization](/docs/virtualization/),
[thumbnail scrubbing](/docs/thumbnail-scrubbing/),
[video facades](/docs/video-facades/),
[custom icons](/docs/custom-icons/),
[localization and RTL](/docs/localization-rtl/) and
[responsive loading](/docs/responsive-loading/).
- The [settings reference](/docs/settings/), every setting is an input
of the same name, and the [events](/docs/events/) list, each one an
output here.
## License
Free and open source under the GPLv3, like every lightGallery package. If
your project keeps its source proprietary, a [commercial license](/license/)
covers it: same code, nothing gated. Use `0000-0000-000-0000` as a temporary
`licenseKey` for evaluation.
---
# Justified layout
> Lay gallery thumbnails out in justified rows, equal heights, varying widths, filling the container edge to edge, in vanilla JavaScript, React, Vue and Angular.
Justified thumbnail rows for every lightGallery package, no extra dependencies.
Canonical page: https://www.lightgalleryjs.com/docs/justified-layout/
lightGallery v3 ships a **justified layout** for the trigger
thumbnails: rows of equal height and varying widths that fill the
container edge to edge, like the photo grids on popular photography
sites. The row math lives in
[`@lightgallery/headless`](/docs/headless/) and is shared by all
four packages, so the same options produce the same grid everywhere. The
[justified layout demo](/demos/justified-layout/) shows it running with
the code for each stack.
How it works, in every package:
- **Aspect ratios** come from the trigger's `data-lg-size`
attribute, then the thumbnail's `width`/`height` attributes, then
the loaded image's natural size (one relayout when the last
unknown resolves). Provide `data-lg-size` for a layout with zero
reflows.
- The grid **reflows on container resize** (one `ResizeObserver`),
mirrors automatically under **RTL**, and writes a precise `sizes`
attribute on thumbnails that carry `srcset`.
- `lastRow` controls the leftover row: `'start'` (default) keeps it
at the natural height aligned to the reading start, `'justify'`
stretches it edge to edge, `'hide'` hides the leftover
thumbnails.
- `maxScale` clamps how tall a sparse row may grow, as a multiple
of the target row height.
All packages share one stylesheet, it is not part of the bundle, so
galleries that do not use the layout pay zero bytes:
```js
import 'lightgallery/css/lg-justified.css';
```
## How the grid loads
A justified grid cannot be positioned until the script has measured
the container, so the stylesheet keeps the thumbnails out of flow and
invisible until the layout has run. Nothing is ever seen unorganised,
and an oversized thumbnail cannot widen the page in the meantime.
Put the container class in your **markup**, not just in the script.
The plugin adds it too, but only once it runs, and the flash you are
avoiding happens before that:
```html
```
Once the layout has positioned a trigger, its box shows immediately as
a placeholder and the thumbnail fades in on top when it has loaded, so
the rows fill in as the images arrive. `justifiedReveal` picks the
order:
- `'row'` (default) reveals whole rows top to bottom, each once
every thumbnail in it has loaded.
- `'image'` reveals each thumbnail on its own, as soon as it has
loaded.
Two details worth knowing. Before the layout runs the container has no
height, so content below it shifts down when the rows appear; give the
container a `min-height` if that matters on your page. And the
placeholder and fade only apply to grids lightGallery lays out, so
your own layout code using the same class names is left alone.
## Vanilla JavaScript
The layout ships as a regular plugin. Adding it to `plugins` lays the
gallery's inline triggers out; dynamic galleries (no trigger grid) are
left untouched. The grid follows `refresh()` and `updateSlides()`, so
adding or removing triggers relays the rows out automatically.
```js
import lightGallery from 'lightgallery';
import lgJustified from 'lightgallery/plugins/justified';
import lgThumbnail from 'lightgallery/plugins/thumbnail';
import lgZoom from 'lightgallery/plugins/zoom';
import 'lightgallery/css/lightgallery.css';
import 'lightgallery/css/lg-justified.css';
lightGallery(document.getElementById('gallery'), {
plugins: [lgJustified, lgThumbnail, lgZoom],
justifiedRowHeight: 180,
justifiedGap: 8,
justifiedLastRow: 'start',
});
```
| Setting | Default | Description |
| --- | --- | --- |
| `justified` | `true` | Enable the layout (the plugin must be in `plugins`) |
| `justifiedRowHeight` | `180` | Row height (px) the layout aims for |
| `justifiedGap` | `8` | Gap between thumbnails and between rows (px) |
| `justifiedLastRow` | `'start'` | Leftover-row policy: `'justify'`, `'start'` or `'hide'` |
| `justifiedMaxScale` | `1.75` | Row-height clamp as a multiple of `justifiedRowHeight` |
| `justifiedReveal` | `'row'` | Reveal order as thumbnails load: `'row'` or `'image'` |
The [settings reference](/docs/settings/#justified-layout-plugin) carries
the generated descriptions for the same six settings.
## React
Wrap the `LightGalleryItem` triggers in the `JustifiedGrid` component.
The rendered markup stays unpositioned until the first client-side
measure, so SSR output is hydration-safe.
```tsx
import { LightGallery, LightGalleryItem } from '@lightgallery/react';
import { JustifiedGrid } from '@lightgallery/react/plugins/justified';
import Thumbnail from '@lightgallery/react/plugins/thumbnail';
import Zoom from '@lightgallery/react/plugins/zoom';
import 'lightgallery/css/lightgallery.css';
import 'lightgallery/css/lg-justified.css';
{items.map((item) => (
))}
;
```
## Vue
```vue
```
## Angular
```ts
import { LgJustifiedGridComponent } from '@lightgallery/angular/plugins/justified';
import 'lightgallery/css/lightgallery.css';
import 'lightgallery/css/lg-justified.css';
```
```html
@for (item of items; track item.src) {
}
```
## Component props (React, Vue, Angular)
The three framework components share one prop surface:
| Prop | Default | Description |
| --- | --- | --- |
| `rowHeight` | `180` | Row height (px) the layout aims for |
| `gap` | `8` | Gap between thumbnails and between rows (px) |
| `lastRow` | `'start'` | Leftover-row policy: `'justify'`, `'start'` or `'hide'` |
| `maxScale` | `1.75` | Row-height clamp as a multiple of `rowHeight` |
| `reveal` | `'row'` | Reveal order as thumbnails load: `'row'` or `'image'` |
| `direction` | `'auto'` | Reading direction; `'auto'` inherits the nearest ancestor `dir` attribute |
## Standalone grids
The row math itself is a pure function you can use without a gallery:
```js
import { getJustifiedLayout } from '@lightgallery/headless';
const { boxes, containerHeight } = getJustifiedLayout({
ratios: [1.5, 0.66, 1.5, 1],
containerWidth: 960,
targetRowHeight: 180,
gap: 8,
lastRow: 'justify',
});
// boxes: [{ top, start, width, height }, ...] in reading order.
```
`start` offsets are logical, measured from the reading-start edge, so the same output positions a grid in LTR and RTL.
---
# Localization & RTL
> Localize every lightGallery label from one strings object and mirror the gallery for right-to-left pages, in vanilla JavaScript, React, Vue and Angular.
One strings contract for every label, and an opt-in RTL layer that mirrors the whole gallery.
Canonical page: https://www.lightgalleryjs.com/docs/localization-rtl/
## The `strings` object
Every user-facing label in lightGallery v3, core controls **and**
plugin buttons, lives in a single `strings` setting. Pass a partial
object; the keys you provide merge over the English defaults. The same
contract works in all four packages, and the
[settings reference](/docs/settings/#strings) carries the generated
description.
```js
lightGallery(el, {
plugins: [lgThumbnail, lgZoom],
strings: {
closeGallery: 'Galerie schließen',
previousSlide: 'Vorheriges Bild',
nextSlide: 'Nächstes Bild',
zoomIn: 'Vergrößern',
zoomOut: 'Verkleinern',
slideAnnouncement: 'Bild {index} von {total}',
},
});
```
```tsx
```
```vue
```
```html
```
### Core keys
| Key | Default |
| --- | --- |
| `closeGallery` | `'Close gallery'` |
| `toggleMaximize` | `'Toggle maximize'` |
| `previousSlide` | `'Previous slide'` |
| `nextSlide` | `'Next slide'` |
| `download` | `'Download'` |
| `playVideo` | `'Play video'` |
| `mediaLoadingFailed` | `'Oops... Failed to load content...'` |
| `galleryLabel` | `'Gallery'` |
| `slideAnnouncement` | `'Image {index} of {total}'` |
| `moreOptions` | `'More options'` |
`galleryLabel` names the gallery dialog for assistive technology when
`ariaLabelledby` is not set. `slideAnnouncement` is announced politely
on every slide change, `{index}` and `{total}` are replaced with the
1-based position and the slide count, and the slide caption, when
present, is appended.
### Plugin labels
Plugin buttons read from the same object, no per-plugin
configuration needed:
| Key | Default | Plugin |
| --- | --- | --- |
| `share` | `'Share'` | Share |
| `toggleThumbnails` | `'Toggle thumbnails'` | Thumbnail |
| `toggleAutoplay` | `'Toggle Autoplay'` | Autoplay |
| `toggleFullscreen` | `'Toggle Fullscreen'` | Fullscreen |
| `zoomIn` | `'Zoom in'` | Zoom |
| `zoomOut` | `'Zoom out'` | Zoom |
| `viewActualSize` | `'View actual size'` | Zoom |
| `rotateLeft` | `'Rotate left'` | Rotate |
| `rotateRight` | `'Rotate right'` | Rotate |
| `flipHorizontal` | `'Flip horizontal'` | Rotate |
| `flipVertical` | `'Flip vertical'` | Rotate |
| `toggleComments` | `'Toggle Comments'` | Comment |
### Deprecated per-plugin objects
The v2-era per-plugin label objects (`zoomPluginStrings`,
`thumbnailPluginStrings`, `autoplayPluginStrings`,
`fullscreenPluginStrings`, `rotatePluginStrings`,
`sharePluginStrings`, `commentPluginStrings`) still work as
**deprecated aliases**: a key set explicitly on a legacy object wins
over the same key on `strings`, so existing configurations keep their
behavior. New code should use `strings` only.
## Right-to-left galleries
Set `direction: 'rtl'` and load the opt-in RTL stylesheet, keyboard
arrows, swipe advance, the slide transforms and the thumbnail strip
all mirror. The layer is a separate file, so LTR galleries pay zero
CSS bytes. The [RTL demo](/demos/rtl/) shows a mirrored gallery with
the code for each stack.
```js
import 'lightgallery/css/lg-rtl.css';
lightGallery(el, {
plugins: [lgThumbnail, lgZoom],
direction: 'rtl',
});
```
```tsx
import 'lightgallery/css/lg-rtl.css';
;
```
```vue
```
```ts
import 'lightgallery/css/lg-rtl.css';
```
```html
```
In Angular, `withRtl()` is the same setting as feature-list sugar, `[features]="[withRtl(), withThumbnail()]"`, and an explicit
`[direction]` input still wins.
### The `direction` setting
| Value | Behavior |
| --- | --- |
| `'ltr'` (default) | Left-to-right, exactly as before |
| `'rtl'` | Mirrored: arrows, swipe, transforms, thumbnails |
| `'auto'` | Inherit the direction of the page/gallery element |
`'auto'` resolves from the gallery element's computed direction in
vanilla JavaScript, and from the nearest `dir` attribute
(`document.documentElement` / `document.body`) in the framework
packages. The default stays `'ltr'` so upgrading never changes
behavior on existing pages, RTL pages opt in with `'rtl'` or
`'auto'`.
### What the RTL layer covers
- Prev/next arrows swap sides and glyphs; keyboard `ArrowLeft` /
`ArrowRight` advance in reading order.
- Slide resting positions, swipe advance and the release spring are
mirrored.
- The thumbnail strip starts at the right edge and drags in reading
order; the counter keeps its `3 / 5` form in bidi text.
- The share dropdown, comment drawer and toolbar chrome mirror.
- Slide captions render `direction: rtl`, while the slide rail
keeps internal LTR geometry (the mirroring lives in the
transforms).
Everything is scoped to `.lg-container[dir='rtl']`, a forced-LTR
gallery inside an RTL page stays untouched. The decorative horizontal
transitions (`lg-slide-skew`, `lg-tube`, …) intentionally keep their
LTR choreography.
---
# Virtualization
> Keep thousand-item galleries fast: a bounded slide pool and a windowed thumbnail strip, one setting in vanilla JavaScript, React, Vue and Angular.
Galleries with thousands of items keep a small, constant DOM, one setting, every package.
Canonical page: https://www.lightgalleryjs.com/docs/virtualization/
Large galleries pay for every DOM node: a 1,000-item gallery that
renders 1,000 thumbnails plus mounted slides gets slow to open,
slow to scroll and heavy on memory. lightGallery v3 adds an opt-in
`virtualization` setting that bounds both:
- **Slide pool**, only the current slide, its neighbors and any
protected slides stay mounted. `slides` sets the pool size.
- **Thumbnail windowing**, only the visible thumbnails plus an
overscan render; leading and trailing **spacers** preserve the
strip's total width, so scrollbar-free dragging, the pager math
and the strip geometry are identical to a fully rendered strip.
`thumbs` sets the overscan per side, or `'auto'` derives one
extra viewport per side.
The setting is **off by default** (`undefined`), the classic
behavior: every thumbnail renders and the mounted-slide window
follows `numberOfSlideItemsInDom`. The shape is shared by all four
packages, and the [settings reference](/docs/settings/#virtualization)
carries the generated description. The
[1,000-slide stress demo](/demos/virtualization/) shows the mounted-slide
counter while you navigate.
```ts
virtualization: {
slides: 7, // mounted-slide pool size
thumbs: 'auto', // thumb overscan per side: a number, or 'auto'
}
```
## Zero-cost interaction
The thumbnail window advances only at **commit points**, release,
slide change, resize, never per `pointermove`. Dragging the strip
costs nothing extra; the overscan covers the in-flight stretch. A
fast fling renders its whole flight corridor before the glide starts,
so the strip never shows blank thumbnails mid-flight, and shrinks
back to the normal window when it settles.
## Vanilla JavaScript
```js
import lightGallery from 'lightgallery';
import lgThumbnail from 'lightgallery/plugins/thumbnail';
import lgZoom from 'lightgallery/plugins/zoom';
lightGallery(el, {
dynamic: true,
dynamicEl: items, // thousands of items
plugins: [lgThumbnail, lgZoom],
virtualization: {
slides: 7,
thumbs: 'auto',
},
});
```
## React
```tsx
```
## Vue
```vue
```
## Angular
```html
```
## Options
| Key | Default | Description |
| --- | --- | --- |
| `slides` | `numberOfSlideItemsInDom` | Mounted-slide pool size, how many slide elements exist at once, regardless of gallery length |
| `thumbs` | off | Thumbnail-strip windowing: overscan thumbs kept mounted on each side of the visible range, or `'auto'` for one extra viewport per side |
Notes:
- Omit the whole `virtualization` object to keep the classic
render-everything behavior, nothing changes for existing
galleries.
- A zoomed slide is never unmounted: zoom only ever lives on the
current slide, which is always inside the pool.
- Thumbnail windowing needs the Thumbnail plugin; `slides` works on
its own.
---
# Video facades
> Provider video slides load as lightweight poster facades, the YouTube, Vimeo or Wistia iframe is created only when the user presses play.
Poster-first video slides: the provider iframe loads only when the user presses play.
Canonical page: https://www.lightgalleryjs.com/docs/video-facades/
A provider video embed (YouTube, Vimeo, Wistia) drags a lot of
JavaScript into the page the moment its iframe mounts. In v3 the
video plugin renders provider slides as **lite facades** by default: a
poster image with a play button, visually identical to the loaded
player. The real iframe is created only when the user presses play, sliding past a video costs nothing.
```js
lightGallery(el, {
plugins: [lgVideo],
// Facades are the default, set false for the classic
// eager-iframe behavior on all provider slides.
videoFacade: true,
});
```
The [video facades demo](/demos/video-facades/) shows a facade next to
an eager embed, and the [settings reference](/docs/settings/#videoFacade)
carries the generated description.
The setting ships in the video plugin and works identically in all
four packages (`videoFacade` prop/setting alongside the Video
plugin).
## The poster chain
The facade needs an image. It resolves, in order:
1. The item's `poster`.
2. For YouTube slides, the thumbnail endpoint
(`img.youtube.com`), controlled by the existing
`loadYouTubePoster` setting (default `true`).
3. The item's `thumb`.
A slide with **no resolvable poster** keeps the previous
eager-iframe behavior, a facade never renders as an empty box.
Vimeo and Wistia posters are not synthesized from provider endpoints
(those need authenticated APIs); give those slides a `poster` or rely
on the `thumb` chain, with the vimeoThumbnail plugin active, the
fetched Vimeo thumb feeds the chain automatically.
Two settings intentionally bypass the facade: `autoplayFirstVideo`
(default `true`) and `autoplayVideoOnSlide` materialize the iframe
immediately, an autoplaying facade would defeat both.
HTML5 `