@lightgallery/react
A native React lightGallery component. React renders the triggers and the lightbox, with no wrapper and no second runtime touching your DOM.
@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
npm install @lightgallery/react lightgallery
Peer range: React 18 or 19 (react and react-dom).
// 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)
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: <h4>Mountains</h4>,
},
];
export function Gallery() {
return (
<LightGallery plugins={[Thumbnail, Zoom]} zoom={{ scale: 1.5 }}>
{items.map((item) => (
<LightGalleryItem key={item.src} item={item} href={item.src}>
<img src={item.thumb} alt={item.alt} />
</LightGalleryItem>
))}
</LightGallery>
);
}
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.
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 (
<>
<button onClick={() => setOpen(true)}>Open gallery</button>
<LightGallery
slides={items}
open={open}
onClose={() => setOpen(false)}
index={index}
onIndexChange={setIndex}
/>
</>
);
}
Imperative
A ref exposes openGallery(index?), closeGallery(),
goToSlide(index), nextSlide(), prevSlide() and refresh().
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<LightGalleryRefHandle>(null);
return (
<>
<button onClick={() => gallery.current?.openGallery(1)}>
Open at slide 2
</button>
<LightGallery ref={gallery} slides={items} />
</>
);
}
Settings, events and render slots
Settings are flat props with the vanilla names (mode, speed, loop,
captionPosition, …); the settings reference 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
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 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 (<LightGalleryItem>) server-render as static markup, so grids
are crawlable.
With the Next.js App Router the component must live in a client component:
'use client';
import { LightGallery, LightGalleryItem } from '@lightgallery/react';
// ... as above
Import the CSS in your root layout (or any server component):
// app/layout.tsx
import 'lightgallery/css/lightgallery.css';
No dynamic(() => …, { ssr: false }) wrapper is needed. The
Next.js image gallery demo 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 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/ | thumbWidth, thumbHeight, thumbMargin, animateThumb, toggleThumb |
| Zoom | @lightgallery/ | scale, actualSize, showZoomInOutIcons, infiniteZoom, enableZoomAfter |
| Video | @lightgallery/ | autoplayFirstVideo, autoplayVideoOnSlide, youTubePlayerParams, vimeoPlayerParams, gotoNextSlideOnVideoEnd |
| Autoplay | @lightgallery/ | slideShowAutoplay, slideShowInterval, progressBar, forceSlideShowAutoplay |
| Fullscreen | @lightgallery/ | fullScreen |
| Hash | @lightgallery/ | galleryId, customSlideName |
| Pager | @lightgallery/ | pager |
| Share | @lightgallery/ | facebook, twitter, pinterest, additionalShareOptions (typed objects) |
| Rotate | @lightgallery/ | rotateSpeed, rotateLeft/, flipHorizontal/ |
| Comment | @lightgallery/ | commentBox, renderComments(item) render prop |
| MediumZoom | @lightgallery/ | margin, backgroundColor (+ per-item lgBackgroundColor) |
| RelativeCaption | @lightgallery/ | relativeCaption |
| VimeoThumbnail | @lightgallery/ | showVimeoThumbnails, showThumbnailWithPlayButton |
| OriginCrop | @lightgallery/ | originCrop: flies a cropped thumbnail from its crop (zoom from origin) |
| Justified | @lightgallery/ | Not a plugin: the <JustifiedGrid> component wraps the triggers, with rowHeight, gap, lastRow (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; the key changes:
- Items are data:
dynamic/dynamicEland every DOM-scraping option (selector,extraProps,exThumbImage, …) are gone, passslidesor wrap thumbnails in<LightGalleryItem item={…}>. - HTML-string options became typed render props:
subHtml→item.caption(ReactNode) oritem.captionHtml(explicit raw HTML);nextHtml/prevHtml/appendCounterTo→ therenderprop slots. appendSubHtmlTo→captionPosition: 'bar' | 'slide' | 'outer';addClass→className;index→ controlledindex/onIndexChangeordefaultIndex.- Plugins are modules, not constructors:
plugins={[Zoom]}with settings as azoom={{ … }}prop instead of flat settings keys. - Events keep their documented
onXxxnames and payloads;updateSlidesis gone (changingslidesis the update). Thevideojsoption was dropped, bring custom players through a pluginslideRenderer.
Next steps
- React image gallery, video gallery, carousel and video carousel demos, each with the code behind it.
- Features that work the same in every package: justified layout, virtualization, thumbnail scrubbing, video facades, custom icons, localization and RTL and responsive loading.
- The settings reference, every setting is a prop of
the same name, and the 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
covers it: same code, nothing gated. Use 0000-0000-000-0000 as a temporary
licenseKey for evaluation.
lightGallery