# 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
img1 img2
``` #### 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 ( One ); } ``` 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) => ( {item.alt} ))} ); } ``` ## 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) => ( {item.alt} ))} ; ``` ## 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 `