@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.
@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 <script setup> SFCs,
v-model for the open state and index, typed emits, scoped slots, a
Teleport overlay and a tree-shakable subpath per plugin. Styling reuses
the published lightgallery/css/* files, so the lightbox looks exactly
like the vanilla one.
Install
npm install @lightgallery/vue lightgallery
Peer range: vue >=3.4 (uses defineModel).
// Global styles (main.ts or your root stylesheet):
import 'lightgallery/css/lightgallery.css';
// plus the CSS of each plugin 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.
<script setup lang="ts">
import {
LightGallery,
LgItem,
type LgGalleryItem,
type SlideEventDetail,
} from '@lightgallery/vue';
import Thumbnail from '@lightgallery/vue/plugins/thumbnail';
import Zoom from '@lightgallery/vue/plugins/zoom';
const plugins = [Thumbnail, Zoom];
const items: LgGalleryItem[] = [
{
src: 'img/1.jpg',
thumb: 'img/1-t.jpg',
alt: '…',
caption: '…',
lgSize: '1600-1067', // natural size, enables the zoom-from-origin open animation
},
];
function onSlide({ index }: SlideEventDetail) {
console.log('slide', index);
}
</script>
<template>
<LightGallery
:plugins="plugins"
:thumbnail="{ thumbWidth: 120 }"
@after-slide="onSlide"
>
<LgItem v-for="item of items" :key="item.src" :item="item">
<img :src="item.thumb" :alt="item.alt" />
</LgItem>
</LightGallery>
</template>
Controlled
Pass :slides instead of <LgItem> children and the component renders no
triggers; v-model:open and v-model:index give you the state.
<script setup lang="ts">
import { ref } from 'vue';
import { LightGallery, type LgGalleryItem } from '@lightgallery/vue';
const open = ref(false);
const index = ref(0);
const items: LgGalleryItem[] = [
{ src: 'img/1.jpg', thumb: 'img/1-t.jpg', alt: 'Mountains' },
{ src: 'img/2.jpg', thumb: 'img/2-t.jpg', alt: 'Forest' },
];
</script>
<template>
<button @click="open = true">Open gallery</button>
<LightGallery :slides="items" v-model:open="open" v-model:index="index" />
</template>
Imperative
A template ref exposes openGallery(index?), closeGallery(),
goToSlide(index), nextSlide(), prevSlide() and refresh().
<script setup lang="ts">
import { ref } from 'vue';
import { LightGallery, type LgGalleryItem } from '@lightgallery/vue';
const gallery = ref<InstanceType<typeof LightGallery> | null>(null);
const items: LgGalleryItem[] = [
{ src: 'img/1.jpg', thumb: 'img/1-t.jpg', alt: 'Mountains' },
{ src: 'img/2.jpg', thumb: 'img/2-t.jpg', alt: 'Forest' },
];
</script>
<template>
<button @click="gallery?.openGallery(1)">Open at slide 2</button>
<LightGallery ref="gallery" :slides="items" />
</template>
Settings, events and slots
Settings are same-named props (:mode, :speed, :loop,
:caption-position, …); the settings reference lists
every one. Events are kebab-case emits of the documented
event names 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 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/<name> 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/ | thumbWidth, thumbHeight, thumbMargin, animateThumb, toggleThumb |
| zoom | plugins/ | scale, actualSize, showZoomInOutIcons, infiniteZoom, enableZoomAfter |
| video | plugins/ | autoplayFirstVideo, autoplayVideoOnSlide, youTubePlayerParams, vimeoPlayerParams, gotoNextSlideOnVideoEnd |
| autoplay | plugins/ | slideShowAutoplay, slideShowInterval, progressBar, forceSlideShowAutoplay |
| fullscreen | plugins/ | fullScreen |
| hash | plugins/ | galleryId, customSlideName |
| pager | plugins/ | pager |
| share | plugins/ | facebook, twitter, pinterest, additionalShareOptions (typed objects) |
| rotate | plugins/ | rotateSpeed, rotateLeft/, flipHorizontal/ |
| comment | plugins/ | commentBox; the comment body comes from the #comments slot |
| mediumZoom | plugins/ | margin, backgroundColor (+ per-item lgBackgroundColor) |
| relativeCaption | plugins/ | relativeCaption (presets captionPosition: 'slide') |
| vimeoThumbnail | plugins/ | showVimeoThumbnails, showThumbnailWithPlayButton |
| originCrop | plugins/ | originCrop: flies a cropped thumbnail from its crop (zoom from origin) |
| justified | plugins/ | Not a plugin: the <JustifiedGrid> component wraps the triggers, with row-height, gap, last-row (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
opentrue at first render), the<Teleport>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
<ClientOnly>wrapper needed. Deep-link flows (hash plugin) run after hydration. - Import the CSS globally (
nuxt.configcss: ['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 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; the key changes:
dynamicEl→:slides(typedLgGalleryItem[]), or<LgItem>trigger components for uncontrolled galleries.onAfterSlideetc. → kebab-case emits without the prefix:@after-slide.appendSubHtmlTo→captionPosition: 'bar' | 'slide' | 'outer';subHtmlstrings →caption(plain string), the#captionslot, or the explicit raw-HTMLcaptionHtmlopt-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 and video gallery 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 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
covers it: same code, nothing gated. Use 0000-0000-000-0000 as a temporary
licenseKey for evaluation.
lightGallery