Vanilla JavaScript Image Gallery & Lightbox
Beautiful, responsive, zero dependencies.
Live demo: https://neikiri.dev/gallery
Neiki's Gallery is a production-ready image gallery and lightbox library built with vanilla JavaScript and CSS. No frameworks, no dependencies — just drop two files into your project and you're ready to go.
- 4 Layouts — Masonry, Grid, Mosaic (
data-size), and Filmstrip (horizontal scroll) - Glassmorphism lightbox — backdrop-blur overlay with floating toolbar
- Slideshow / Autoplay — animated progress bar, play/pause, configurable interval
- Comparison slider —
NeikiGallery.compare()for before/after image comparison - Share popup — copy link to clipboard or download image from lightbox
- Contextual zoom — zoom to clicked point with pan support
- Batch select — Shift+click multi-select with checkmark overlays
- Tag filtering — auto-generated pill filter bar from
data-tags - Staggered entrance — sequential fade-in animation on load
- Keyboard navigation — arrows, Escape, Home/End, F (fullscreen), Space (slideshow)
- Touch / swipe — mobile-friendly left/right swipe navigation
- Lazy loading — IntersectionObserver with shimmer placeholders
- Image captions — bottom-sheet style on mobile
- Thumbnail strip — scrollable thumbnails with scale hover effect
- Zoom — click to toggle zoom, or contextual zoom to cursor point
- Fullscreen — native Fullscreen API
- Pill counter — glassmorphism pill-shaped counter badge
- Toast notifications — for share/copy actions
- Deep linking — URL hash navigation
- Multiple instances — independent galleries on one page
- Dark & light themes — CSS custom properties + accent color system (
--neiki-accent) - Accessibility — ARIA attributes, focus management,
prefers-reduced-motion - Event system —
open,close,change,filter,select,slideshowStart,slideshowStop,favorite,unfavorite,infoOpen,infoClose,print,editorOpen,editorExport,annotateOpen,annotateExport,append,remove - Blurhash placeholders —
data-blurhashattribute decoded into blurred preview (replaces shimmer) - Story mode — Instagram-like vertical fullscreen viewer with progress bars and tap navigation
- Picture-in-Picture — minimize lightbox to a resizable corner window
- Image focus point —
data-focus="0.3 0.7"controlsobject-positionfor smart cropping - EXIF overlay — camera model, focal length, aperture, shutter speed, ISO from JPEG binary
- Color palette extraction — k-means quantized 5-color palette strip in lightbox
- Backdrop tint — overlay adapts to dominant color of current image
- FLIP morph transition — smooth thumbnail-to-lightbox animation
- Virtual scrolling —
content-visibilityvirtualization for large galleries (50+ items) - Drag & drop reorder — HTML5 drag to reorder grid items
- Aspect-ratio skeleton —
data-width/data-heightfor placeholder sizing - Web Share API — native mobile sharing with clipboard fallback
- Video & embeds — auto-detect MP4/WebM/Ogg/MOV/M4V, YouTube and Vimeo URLs in lightbox
- Plugin system —
NeikiGallery.registerPlugin()withinit/open/change/close/destroylifecycle hooks - Album groups —
data-group="album"links galleries; navigation traverses across all - Favorites — heart button + localStorage persistence +
Bkey shortcut - Info panel — slide-out sidebar with metadata, EXIF, source URL (
Ikey) - Image editor — rotate / flip / export to PNG via
<canvas> - Annotation layer — freehand drawing with color picker, brush size, undo, clear, export
- Print — print current image with caption (
Pkey) - Right-click context menu — open original, copy link, download, share, print, favorite
- Infinite scroll —
loadMorecallback +append()/remove()API - Keyboard shortcuts overlay — press
?to view all shortcuts - System theme detection —
theme: 'system'follows OSprefers-color-scheme - Kenburns slideshow — slow zoom-and-pan animation per slide
- Configurable counter —
counterFormatwith{current}/{total}/{percent}tokens - Cross-browser — Chrome 80+, Firefox 75+, Safari 14+, Edge 80+
Add this single line to your HTML — that's all you need (CSS is included automatically):
<script src="https://cdn.neikiri.dev/neiki-gallery/neiki-gallery.min.js"></script>Then create your gallery markup:
<div data-neiki-gallery data-layout="masonry" data-theme="dark">
<a href="images/photo1-full.jpg" data-caption="Beautiful sunset">
<img src="images/photo1-thumb.jpg" alt="Sunset">
</a>
<a href="images/photo2-full.jpg" data-caption="Mountain view">
<img src="images/photo2-thumb.jpg" alt="Mountains">
</a>
</div>That's it — galleries with data-neiki-gallery auto-initialize on page load.
Unminified files are also available if you need them for debugging (require separate CSS):
https://cdn.neikiri.dev/neiki-gallery/neiki-gallery.js https://cdn.neikiri.dev/neiki-gallery/neiki-gallery.css
Pin a specific version to avoid unexpected changes:
https://cdn.neikiri.dev/neiki-gallery/3.0.0/neiki-gallery.min.js
Download or clone the repository from GitHub:
git clone https://github.com/neikiri/neiki-gallery.gitFor production, you only need one file — CSS is already included:
<script src="path/to/neiki-gallery.min.js"></script>For development / debugging, use the unminified versions (requires separate CSS):
<link rel="stylesheet" href="path/to/neiki-gallery.css">
<script src="path/to/neiki-gallery.js"></script>The dist/ folder contains:
| File | Description |
|---|---|
neiki-gallery.min.js |
Minified JS + CSS included (production) |
neiki-gallery.min.css |
Minified CSS standalone (optional) |
neiki-gallery.js |
Full JS without CSS (development / debugging) |
neiki-gallery.css |
Full CSS (development / debugging) |
const gallery = new NeikiGallery('#my-gallery', {
layout: 'masonry', // 'masonry' | 'grid' | 'mosaic' | 'filmstrip'
loop: true,
thumbnails: true,
zoom: true,
contextualZoom: false, // zoom to click point
fullscreen: true,
transition: 'fade', // 'fade' | 'slide'
theme: 'system', // 'dark' | 'light' | 'system' (auto-detect) ← v3 default
hashNavigation: true,
counter: true,
counterFormat: '{current} / {total}', // v3 — tokens: {current}, {total}, {percent}
stagger: true,
slideshow: { // v3 — nested config (boolean still works)
interval: 4000,
pauseOnHover: true,
kenburns: false,
direction: 'forward' // or 'reverse'
},
share: true,
filter: false,
batchSelect: false,
// v2.1.0
focusPoint: true,
blurhash: true,
exif: false,
storyMode: false,
pip: false,
virtualScroll: false,
dragReorder: false,
backdropTint: false,
morphTransition: false,
colorPalette: false,
aspectSkeleton: true,
// v3.0.0
video: true, // detect MP4 / YouTube / Vimeo
plugins: ['watermark', { name: 'analytics', trackingId: 'X' }],
group: '', // album group name (or use data-group)
favorites: false, // ❤️ button + localStorage
favoritesKey: '', // custom localStorage key suffix
infoPanel: false, // sidebar with metadata (I key)
contextMenu: false, // right-click menu on items
shortcutsHelp: true, // overlay on '?' key
infiniteScroll: false, // auto-load more
loadMore: null, // function(currentLength) → array | Promise<array>
editor: false, // crop/rotate/flip toolbar
annotate: false // freehand drawing layer
});gallery.open(index); // Open lightbox at image index
gallery.close(); // Close lightbox
gallery.next(); // Go to next image (group-aware)
gallery.prev(); // Go to previous image (group-aware)
gallery.startSlideshow(); // Start autoplay
gallery.stopSlideshow(); // Stop autoplay
gallery.pauseSlideshow(); // v3 — pause without emitting stop
gallery.toggleSlideshow(); // Toggle autoplay
gallery.filter('landscape'); // Filter by tag (null = show all)
gallery.getSelected(); // Get batch-selected items
gallery.clearSelection(); // Clear batch selection
gallery.getOrder(); // Get current item order (after drag reorder)
// v3.0.0 — Favorites
gallery.toggleFavorite(); // Toggle favorite for current image
gallery.isFavorite(index); // Check if image is favorited
gallery.getFavorites(); // Get array of favorited image src URLs
gallery.clearFavorites(); // Remove all favorites
// v3.0.0 — Info panel / overlays
gallery.toggleInfoPanel(force); // Toggle info sidebar (force = bool)
gallery.toggleShortcutsHelp(force); // Toggle keyboard shortcuts overlay
// v3.0.0 — Editor & annotation
gallery.openEditor(); // Open crop/rotate/flip editor
gallery.closeEditor();
gallery.getEditedBlob(); // Last exported edited image as Blob
gallery.openAnnotate(); // Open drawing layer
gallery.closeAnnotate();
gallery.getAnnotatedBlob();
// v3.0.0 — Print & dynamic content
gallery.print(index); // Print current or specified image
gallery.append([{src,...}]); // Append new items at runtime
gallery.remove(index); // Remove item by index
// v3.0.0 — Plugin access
gallery.plugin('watermark'); // Get plugin instance by name
// v3.0.0 — Event helpers
gallery.on('change', fn);
gallery.off('change', fn); // Remove specific listener
gallery.off('change'); // Remove all listeners for event
gallery.once('open', fn); // Auto-removed after first fire
gallery.destroy(); // Remove gallery, clean up all listeners & DOM// Comparison slider
const slider = NeikiGallery.compare('#compare-container', {
before: 'before.jpg',
after: 'after.jpg',
labelBefore: 'Before',
labelAfter: 'After',
startPosition: 50
});
slider.setPosition(30); // Move handle to 30%
slider.destroy(); // Clean up
// Color palette extraction (v2.1.0)
NeikiGallery.extractPalette('photo.jpg', 5, (colors) => {
console.log(colors); // [{r, g, b, hex}, ...]
});
// Dominant color (v2.1.0)
NeikiGallery.extractDominantColor('photo.jpg', (color) => {
console.log(color); // {r, g, b, hex}
});
// EXIF parsing (v2.1.0)
NeikiGallery.parseExif('photo.jpg', (tags) => {
console.log(tags); // {make, model, iso, fNumber, exposure, focalLength}
});
// Blurhash decoding (v2.1.0)
const pixels = NeikiGallery.decodeBlurhash('LEHV6nWB2y...', 32, 32);
// Media type detection (v3.0.0)
NeikiGallery.detectMediaType('https://youtube.com/watch?v=abc'); // 'youtube'
NeikiGallery.detectMediaType('clip.mp4'); // 'video'
NeikiGallery.detectMediaType('photo.jpg'); // 'image'
// Plugin registration (v3.0.0)
NeikiGallery.registerPlugin('watermark', (gallery, options) => ({
init() { /* set up DOM, listeners */ },
open() { /* on lightbox open */ },
change(d){ /* on slide change, d = {index, item, prevIndex} */ },
close() { /* on lightbox close */ },
destroy(){ /* cleanup */ }
}));
NeikiGallery.unregisterPlugin('watermark');
NeikiGallery.getRegisteredPlugins(); // ['watermark', ...]
// Version
console.log(NeikiGallery.version); // '3.0.0'A plugin is a factory function that receives the gallery instance and an options object, and returns an object with optional lifecycle methods:
NeikiGallery.registerPlugin('analytics', (gallery, opts) => ({
init() {
console.log('[analytics] gallery initialized');
},
open(data) {
track('lightbox_open', { index: data.index });
},
change(data) {
track('slide_view', { src: data.item.src, index: data.index });
},
close() {
track('lightbox_close');
},
destroy() {
console.log('[analytics] cleanup');
}
}));
new NeikiGallery('#g', { plugins: [{ name: 'analytics', sampleRate: 0.5 }] });You can access plugin instances on a gallery via gallery.plugin('analytics').
gallery.on('open', (index) => {
console.log('Opened image', index);
});
gallery.on('close', () => {
console.log('Lightbox closed');
});
gallery.on('change', (index) => {
console.log('Now showing image', index);
});
gallery.on('filter', (tag) => {
console.log('Filtered by:', tag);
});
gallery.on('select', (indices) => {
console.log('Selected:', indices);
});
gallery.on('slideshowStart', () => {
console.log('Slideshow started');
});
gallery.on('reorder', (order) => {
console.log('New order:', order);
});
gallery.on('pipEnter', () => console.log('PiP on'));
gallery.on('storyEnter', () => console.log('Story opened'));
// v3.0.0 events
gallery.on('favorite', ({ index, src }) => console.log('★ favorited', src));
gallery.on('unfavorite', ({ index, src }) => console.log('☆ removed'));
gallery.on('infoOpen', () => console.log('info panel opened'));
gallery.on('print', ({ index, src }) => console.log('printed', src));
gallery.on('editorExport', ({ blob, url }) => console.log('edited blob', url));
gallery.on('annotateExport', ({ blob, url }) => console.log('annotated', url));
gallery.on('append', (items) => console.log('appended', items.length));
gallery.on('remove', ({ index }) => console.log('removed item', index));| Option | Type | Default | Description |
|---|---|---|---|
layout |
string |
'masonry' |
'masonry' · 'grid' · 'mosaic' · 'filmstrip' |
loop |
boolean |
false |
Enable infinite loop navigation |
thumbnails |
boolean |
true |
Show thumbnail strip in lightbox |
zoom |
boolean |
true |
Enable zoom on image click |
contextualZoom |
boolean |
false |
Zoom to cursor point instead of center |
fullscreen |
boolean |
true |
Show fullscreen button |
transition |
string |
'fade' |
'fade' or 'slide' |
theme |
string |
'system' |
'dark' · 'light' · 'system' (auto-detect) |
hashNavigation |
boolean |
true |
URL hash deep linking |
counter |
boolean |
true |
Show image counter |
counterFormat |
string |
'{current} / {total}' |
Counter format with {current}, {total}, {percent} tokens |
captions |
boolean |
true |
Show image captions |
preload |
number |
1 |
Adjacent images to preload |
lazyLoad |
boolean |
true |
Lazy load grid thumbnails |
stagger |
boolean |
true |
Staggered entrance animation |
slideshow |
boolean | object |
false |
true or { interval, pauseOnHover, kenburns, direction } |
share |
boolean |
true |
Show share button in lightbox |
filter |
boolean |
false |
Enable tag filtering bar |
batchSelect |
boolean |
false |
Enable Shift+click multi-select |
focusPoint |
boolean |
true |
Respect data-focus for object-position |
blurhash |
boolean |
true |
Decode data-blurhash placeholders |
exif |
boolean |
false |
Show EXIF data overlay in lightbox |
storyMode |
boolean |
false |
Enable story mode button in toolbar |
pip |
boolean |
false |
Enable PiP button in toolbar |
virtualScroll |
boolean |
false |
Virtualize grid for 50+ items |
dragReorder |
boolean |
false |
Enable drag & drop reorder |
backdropTint |
boolean |
false |
Tint overlay with dominant color |
morphTransition |
boolean |
false |
FLIP morph from grid to lightbox |
colorPalette |
boolean |
false |
Show extracted color palette |
aspectSkeleton |
boolean |
true |
Use data-width/data-height for skeleton |
video |
boolean |
true |
Auto-detect MP4 / YouTube / Vimeo URLs |
plugins |
array |
null |
Plugins to instantiate; e.g. ['name', { name, ...opts }] |
group |
string |
'' |
Album group name (also via data-group) |
favorites |
boolean |
false |
Heart button + localStorage |
favoritesKey |
string |
'' |
Custom suffix for localStorage key |
infoPanel |
boolean |
false |
Slide-out metadata sidebar |
contextMenu |
boolean |
false |
Right-click menu on items |
shortcutsHelp |
boolean |
true |
? key shortcuts overlay |
infiniteScroll |
boolean |
false |
Auto-load more on scroll (with loadMore) |
loadMore |
function |
null |
(currentLength) => array | Promise<array> |
editor |
boolean |
false |
Crop/rotate/flip toolbar |
annotate |
boolean |
false |
Freehand drawing layer |
All options can also be set via data- attributes on the container:
<div data-neiki-gallery
data-layout="mosaic"
data-theme="light"
data-transition="slide"
data-loop="true"
data-stagger="true"
data-slideshow="false"
data-slideshow-interval="5000"
data-slideshow-pause-on-hover="true"
data-slideshow-kenburns="true"
data-counter-format="{current} of {total}"
data-share="true"
data-filter="true"
data-batch-select="false"
data-contextual-zoom="true"
data-story-mode="false"
data-pip="false"
data-exif="false"
data-backdrop-tint="false"
data-morph-transition="false"
data-color-palette="false"
data-drag-reorder="false"
data-virtual-scroll="false"
data-aspect-skeleton="true"
data-group="vacation"
data-favorites="true"
data-info-panel="true"
data-context-menu="true"
data-shortcuts-help="true"
data-editor="true"
data-annotate="true"
data-video="true">
<a href="full.jpg" data-tags="landscape,nature" data-size="large" data-width="1200" data-height="800" data-poster="poster.jpg" data-type="image">
<img src="thumb.jpg" alt="Photo" data-focus="0.5 0.3" data-blurhash="LEHV6nWB2y...">
</a>
</div>Neiki's Gallery uses CSS custom properties for full theming control:
:root {
--neiki-columns: 4;
--neiki-gap: 14px;
--neiki-border-radius: 14px;
--neiki-accent: #3b82f6;
--neiki-overlay-bg: rgba(8, 8, 12, 0.85);
--neiki-overlay-backdrop: blur(24px) saturate(1.2);
--neiki-btn-bg: rgba(255, 255, 255, 0.1);
--neiki-btn-color: #fff;
--neiki-caption-color: #fff;
--neiki-spinner-color: var(--neiki-accent);
--neiki-thumb-size: 52px;
--neiki-zoom-scale: 2;
--neiki-stagger-delay: 0.04s;
--neiki-hover-lift: -4px;
--neiki-hover-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
--neiki-transition-duration: 0.3s;
/* ... and more */
}Switch themes at runtime by setting data-theme="light" or data-theme="dark" on the gallery container.
| Browser | Version |
|---|---|
| Chrome | 80+ |
| Firefox | 75+ |
| Safari | 14+ |
| Edge | 80+ |
v3.0.0 raised the minimum to take advantage of
aspect-ratio,backdrop-filter, stableIntersectionObserver, and modern Canvas/Blob APIs. If you need older browser support, pin to v2.1.0.
v3.0.0 contains breaking changes. Most galleries will continue to work, but review the following:
// v2.x
new NeikiGallery('#g', { slideshow: true, slideshowInterval: 5000 });
// v3.0.0 — preferred (fine-grained control)
new NeikiGallery('#g', {
slideshow: { interval: 5000, pauseOnHover: true, kenburns: true }
});
// v3.0.0 — still works (uses defaults)
new NeikiGallery('#g', { slideshow: true });If you relied on the old default 'dark', set it explicitly:
new NeikiGallery('#g', { theme: 'dark' });Old: #neiki-1=3 → New: #gallery-id-or-group-name/3
If you have external links to the old format, update them or set
hashNavigation: false and implement custom routing.
Default output ("3 / 12") is unchanged, but it's now formatted via
counterFormat: '{current} / {total}'. Override with custom tokens.
Minimum Chrome 80+ / Firefox 75+ / Safari 14+ / Edge 80+. If you need older browser support, stay on v2.1.0.
This project is licensed under the MIT License — see the LICENSE file for details.
- Email: neikiri@neikiri.dev
Made with ❤️ for the web community

