wts-image-gallery
v1.0.1
Published
Accessible, dependency-free, framework-agnostic image gallery and lightbox for TypeScript, Web Components and modern frameworks.
Maintainers
Keywords
Readme
wts-image-gallery
A dependency-free, accessible image gallery and lightbox for plain JavaScript, Angular, React, Vue, Svelte, and other browser frameworks.
The package provides two framework-neutral APIs:
WtsImageGallery, a lifecycle-friendly TypeScript controller.<wts-image-gallery>, an optional standards-based Web Component.
Install
npm install wts-image-galleryController API
<div class="gallery" aria-label="Architecture gallery">
<img
src="/images/one.webp"
alt="Concrete building"
data-title="Concrete study"
data-description="A concrete facade in afternoon light."
>
<img
src="/images/two.webp"
alt="Glass building"
data-title="Glass study"
>
</div>import { WtsImageGallery } from 'wts-image-gallery';
const gallery = new WtsImageGallery('.gallery', {
imageSelector: 'img',
zoom: true,
keyboardEnabled: true,
fullScreenEnabled: true,
});
// Optional programmatic controls:
gallery.open(0);
gallery.next();
gallery.zoomIn();
gallery.panTo(20, 10);
gallery.close();
// Call this from the owning view's cleanup hook.
gallery.destroy();Importing wts-image-gallery does not access window or document.
Construct the controller after your view has mounted when rendering on a
server.
Explicit items
Images can be supplied without scanning markup:
const gallery = new WtsImageGallery(document.querySelector('#trigger')!, {
items: [
{
src: '/images/one.webp',
placeholderSrc: '/images/one-blur.webp',
thumbnailSrc: '/images/one-thumb.webp',
srcset: '/images/one-1280.webp 1280w, /images/one-2560.webp 2560w',
sizes: '100vw',
alt: 'Concrete building',
title: 'Concrete study',
description: 'A concrete facade in afternoon light.',
width: 2560,
height: 1707,
},
{
src: '/images/two.webp',
alt: 'Glass building',
title: 'Glass study',
},
],
loop: true,
});
gallery.open();Modern lightbox features
Modern capabilities are progressive enhancements and remain dependency-free:
const gallery = new WtsImageGallery(element, {
nativeDialog: true,
viewTransitions: true,
swipe: true,
swipeThreshold: 48,
filmstrip: { enabled: true, thumbnails: true, windowSize: 15 },
preload: { before: 1, after: 2, cache: true },
cacheSize: 12,
urlSync: {
key: 'gallery',
id: 'architecture',
mode: 'push',
},
direction: 'auto',
locale: 'en',
onBeforeAction: (detail) => {
// Return false to cancel an open, close, navigation, or zoom action.
console.log(detail.action);
},
onAfterAction: (detail) => console.log(detail.action),
onRenderItem: ({ image, item }) => {
image.dataset['assetId'] = item.src;
},
});nativeDialog uses <dialog>.showModal() when available and falls back to
the package's accessible modal implementation. View Transitions are
feature-detected and automatically fall back to an immediate update.
Reduced-motion preferences disable nonessential transitions.
The active image supports placeholder-to-full-image loading, srcset,
sizes, dimensions, CORS mode, fetch priority, decode(), stale-load
protection, error state reporting, and bounded adjacent-image preloading.
Set cache: false to release preload objects after use.
When urlSync is enabled, the current item is encoded in the query string.
Browser Back closes or navigates the gallery, and an existing gallery URL
can open the matching item during construction.
Web Component
Import the optional element entry point once:
import 'wts-image-gallery/element';Then use standard HTML:
<wts-image-gallery
loop
zoom="true"
native-dialog
swipe
filmstrip
view-transitions
direction="auto"
locale="en"
>
<img src="/images/one.webp" alt="Concrete building" data-title="Concrete study">
<img src="/images/two.webp" alt="Glass building" data-title="Glass study">
</wts-image-gallery>Boolean attributes accept an explicit false value:
<wts-image-gallery zoom="false" fullscreen-enabled="false">
<!-- images -->
</wts-image-gallery>The element exposes openAt(index), close(), next(), previous(),
goTo(index), zoomIn(), zoomOut(), resetZoom(), setZoom(value),
setZoomAt(value, x, y), panTo(x, y), toggleFullscreen(), refresh(),
and useDocumentItems().
Declarative interaction attributes include wheel-zoom,
double-click-zoom, pan-enabled, keyboard-enabled, close-on-escape,
close-on-backdrop, fullscreen-enabled, loop, observe, preload, and
inject-styles, plus native-dialog, swipe, swipe-threshold,
filmstrip, filmstrip-window-size, view-transitions, cache,
cache-size, direction, and locale.
Structured urlSync, asymmetric preload, labels, callbacks, and render hooks
can be assigned through the element's options property.
Framework lifecycle examples
Use the controller from the framework's normal mount and cleanup hooks. No framework adapter is required.
Angular
import {
afterNextRender,
Component,
DestroyRef,
ElementRef,
inject,
viewChild,
} from '@angular/core';
import { WtsImageGallery } from 'wts-image-gallery';
@Component({
selector: 'app-gallery',
template: `<div #images><img src="/one.webp" alt="One"></div>`,
})
export class GalleryComponent {
private readonly images =
viewChild.required<ElementRef<HTMLElement>>('images');
private readonly destroyRef = inject(DestroyRef);
constructor() {
afterNextRender(() => {
const gallery = new WtsImageGallery(this.images().nativeElement);
this.destroyRef.onDestroy(() => gallery.destroy());
});
}
}React
import { useEffect, useRef } from 'react';
import { WtsImageGallery } from 'wts-image-gallery';
export function Gallery() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const gallery = new WtsImageGallery(ref.current!);
return () => gallery.destroy();
}, []);
return <div ref={ref}><img src="/one.webp" alt="One" /></div>;
}Vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted, useTemplateRef } from 'vue';
import { WtsImageGallery } from 'wts-image-gallery';
const images = useTemplateRef<HTMLElement>('images');
let gallery: WtsImageGallery | undefined;
onMounted(() => {
gallery = new WtsImageGallery(images.value!);
});
onBeforeUnmount(() => gallery?.destroy());
</script>
<template>
<div ref="images"><img src="/one.webp" alt="One"></div>
</template>Options
| Option | Default | Purpose |
| --- | --- | --- |
| items | scanned from markup | Explicit image data |
| imageSelector | img | Elements discovered inside the target |
| startIndex | 0 | Initially selected image |
| title | enabled, bottom | Title visibility and position |
| description | enabled, bottom | Description visibility and position |
| zoom | true | Zoom controls and shortcuts |
| minZoom / maxZoom | 0.5 / 3 | Zoom limits |
| zoomStep | 0.5 | Zoom increment |
| wheelZoom | true | Mouse-wheel zoom |
| doubleClickZoom | true | Toggle between 1× and 2× |
| panEnabled | true | Drag and touch panning while zoomed |
| keyboardEnabled | true | Arrow, Home, End, +, -, and 0 keys |
| closeOnEscape | true | Escape key closes the dialog |
| closeOnBackdrop | true | Backdrop click closes the dialog |
| fullScreenEnabled | true | Fullscreen control |
| loop | false | Wrap first/last navigation |
| observe | false | Observe markup changes; the element defaults to true |
| preload | true | Adjacent preload boolean, count, or { before, after, cache } |
| cache / cacheSize | true / 12 | Retain prepared images in a bounded LRU cache |
| nativeDialog | true | Prefer native modal dialog behavior with fallback |
| swipe / swipeThreshold | true / 48 | Navigate with horizontal pointer or touch gestures |
| filmstrip | false | Thumbnail navigation; accepts { enabled, thumbnails, windowSize } |
| urlSync | false | Deep linking and browser-history synchronization |
| viewTransitions | true | Feature-detected image/open transition enhancement |
| direction | auto | LTR/RTL navigation and gesture direction |
| locale | document language or en | Modal language metadata; pair with labels for translation |
| injectStyles | true | Inject default scoped styles |
| labels | English labels | Accessible label overrides |
Callbacks are available as onOpen, onClose, onChange, onZoom, and
onError. Extension hooks are available as onBeforeAction,
onAfterAction, and onRenderItem.
Events
The target dispatches bubbling, composed custom events:
wts-image-gallery-openwts-image-gallery-closewts-image-gallery-changewts-image-gallery-zoomwts-image-gallery-error
container.addEventListener('wts-image-gallery-change', (event) => {
console.log((event as CustomEvent).detail);
});Styling
Default styles are injected and scoped to the lightbox. Set CSS custom properties globally, on a programmatic portal, or on the custom element:
:root {
--wts-gallery-z-index: 1200;
--wts-gallery-overlay: rgba(7, 12, 24, 0.9);
--wts-gallery-accent: #7c3aed;
--wts-gallery-image-radius: 14px;
}To manage styles yourself, use injectStyles: false and import:
import 'wts-image-gallery/styles.css';The Web Component exposes stable CSS shadow parts for triggers, portal,
lightbox, backdrop, dialog, toolbar, stage, figure, image,
caption, title, description, status, live-region, filmstrip,
filmstrip-track, thumbnail, thumbnail-current, and each control button. For
example:
wts-image-gallery::part(thumbnail-current) {
outline: 3px solid var(--brand-accent);
}
wts-image-gallery::part(caption) {
backdrop-filter: blur(16px);
}Accessibility
The lightbox uses native modal behavior where available, accessible fallback
dialog semantics, labelled controls, a live image counter, focus trapping,
focus restoration, keyboard and filmstrip navigation, RTL handling,
forced-colors support, and reduced-motion preferences. Plain image triggers
receive keyboard button semantics while mounted and are restored by
destroy(). Mouse-wheel and focal-point double-click zoom, pointer dragging,
two-pointer pinch gestures, and one-pointer swipe navigation are supported.
Meaningful alt text remains the application's responsibility.
Browser support
Modern evergreen browsers supporting ES2020. Fullscreen is hidden when the browser does not expose the Fullscreen API.
The published runtime remains importable on Node.js 18 and newer for SSR. Package development and release commands require a supported Node.js 22.22+, 24.15+, or 26+ release because of the locked test toolchain.
Migrating from the Angular directive
Version 1 is a framework-neutral rewrite and intentionally does not export
WtsImageDirective. See MIGRATION.md.
License
MIT
