
It supports masonry, grid, mosaic, and horizontal filmstrip layouts for photo-heavy websites, portfolios, documentation pages, and media archives.
Features:
- Four gallery layout modes: masonry, grid, mosaic, and filmstrip (horizontal scroll).
- Glassmorphism lightbox with a backdrop-blur overlay and a floating toolbar.
- Slideshow with an animated progress bar, play/pause, and a Ken Burns zoom-and-pan option per slide.
- Before/after image comparison via a dedicated static utility method.
- Tag filtering with an auto-generated pill filter bar from
data-tagsattributes. - Batch multi-select via Shift+click, with checkmark overlays on selected items.
- Contextual zoom to the exact cursor point, with pan support after zooming in.
- Thumbnail strip in the lightbox with a scale hover effect.
- Lazy loading with
IntersectionObserverand shimmer placeholders. - Blurhash placeholder support via a
data-blurhashattribute decoded on load. - EXIF data overlay in the lightbox showing camera model, aperture, ISO, and shutter speed.
- Five-color palette strip extracted from the current image via k-means quantization.
- Backdrop tint that adapts the overlay color to the dominant color of the current image.
- FLIP morph transition for a smooth thumbnail-to-lightbox open animation.
- Story mode for an Instagram-style vertical fullscreen viewer with progress bars.
- Picture-in-picture mode to minimize the lightbox to a resizable corner window.
- Virtual scrolling via
content-visibilityfor galleries with 50 or more items. - Drag-and-drop reorder of grid items via the HTML5 drag API.
- Video support with auto-detection of MP4, WebM, YouTube, and Vimeo URLs.
- Favorites system with heart button and localStorage persistence.
- Slide-out info panel with metadata and EXIF data.
- In-lightbox image editor with rotate, flip, and PNG export via canvas.
- Freehand annotation layer with color picker, brush size, undo, clear, and export.
- Right-click context menu on gallery items.
- Infinite scroll with a
loadMorecallback and runtimeappend/removeAPI. - Full keyboard navigation including arrows, Escape, Home/End, F for fullscreen, and Space for slideshow.
- Touch and swipe support for left/right navigation on mobile.
- URL hash deep linking for direct navigation to a specific image.
- Album groups that link multiple galleries and traverse navigation across all of them.
- Dark, light, and system (OS preference) theme modes via CSS custom properties.
- Accessibility with ARIA attributes, focus management, and
prefers-reduced-motionsupport. - Rich event system covering open, close, change, filter, select, slideshow, favorites, editor export, and more.
- Plugin system with
init,open,change,close, anddestroylifecycle hooks.
Use Cases:
- Photography portfolio sites where visitors need a full-featured lightbox with EXIF data and keyboard navigation.
- E-commerce product pages where before/after comparison or image annotation adds clarity to product details.
- CMS-driven media libraries where infinite scroll and runtime append keep large image sets performant.
- Multi-gallery editorial pages where album groups tie related galleries into a single navigable experience.
How to use it:
1. Download and load Neiki’s Gallery’s JavaScript & stylesheet in the document.
<link rel="stylesheet" href="/dist/neiki-gallery.css"> <script src="/dist/neiki-gallery/neiki-gallery.min.js"></script>
2. Add the data-neiki-gallery attribute to the gallery container. Each link points to the full-size media file, and each nested image acts as the thumbnail.
Galleries with
data-neiki-galleryinitialize automatically after the page loads.
<div data-neiki-gallery data-layout="masonry" data-theme="dark">
<!-- Opens the full-size city image in the lightbox -->
<a href="media/city-rooftop-full.jpg" data-caption="Rooftop view after sunset">
<img src="media/city-rooftop-thumb.jpg" alt="City rooftop at dusk">
</a>
<!-- Opens the full-size forest image in the same gallery -->
<a href="media/forest-trail-full.jpg" data-caption="Morning trail through pine trees">
<img src="media/forest-trail-thumb.jpg" alt="Pine forest trail">
</a>
<!-- Opens the full-size studio image in the same gallery -->
<a href="media/studio-desk-full.jpg" data-caption="Workspace setup for product shots">
<img src="media/studio-desk-thumb.jpg" alt="Studio desk with camera gear">
</a>
</div>3. You can also initialize the gallery manually. All available options:
layout(string): Gallery layout mode. Accepts'masonry','grid','mosaic', or'filmstrip'. Default:'masonry'.loop(boolean): Loops navigation from the last image back to the first. Default:false.thumbnails(boolean): Shows the scrollable thumbnail strip inside the lightbox. Default:true.zoom(boolean): Toggles zoom on image click inside the lightbox. Default:true.contextualZoom(boolean): Zooms toward the cursor point rather than the image center. Default:false.fullscreen(boolean): Shows the native fullscreen button in the toolbar. Default:true.transition(string): Lightbox transition style. Accepts'fade'or'slide'. Default:'fade'.theme(string): Color theme. Accepts'dark','light', or'system'(follows OS preference). Default:'system'.hashNavigation(boolean): Updates the URL hash for deep linking to a specific image. Default:true.counter(boolean): Shows the image counter badge. Default:true.counterFormat(string): Counter display template. Supports{current},{total}, and{percent}tokens. Default:'{current} / {total}'.captions(boolean): Shows thedata-captiontext in the lightbox. Default:true.preload(number): Number of adjacent images to preload. Default:1.lazyLoad(boolean): Lazy loads grid thumbnails viaIntersectionObserver. Default:true.stagger(boolean): Plays a staggered fade-in animation when the grid loads. Default:true.slideshow(boolean | object): Enables slideshow autoplay. Passtrueor a config object withinterval,pauseOnHover,kenburns, anddirection. Default:false.share(boolean): Shows the share/download button in the lightbox toolbar. Default:true.filter(boolean): Renders a tag filter bar above the gallery fromdata-tagsvalues. Default:false.batchSelect(boolean): Enables Shift+click multi-select with checkmark overlays. Default:false.focusPoint(boolean): Readsdata-focuson thumbnails to setobject-positionfor smart cropping. Default:true.blurhash(boolean): Decodesdata-blurhashattributes into blurred placeholders. Default:true.exif(boolean): Shows an EXIF data overlay in the lightbox for JPEG images. Default:false.storyMode(boolean): Adds an Instagram-style vertical fullscreen viewer button. Default:false.pip(boolean): Adds a picture-in-picture button to minimize the lightbox. Default:false.virtualScroll(boolean): Appliescontent-visibilityvirtualization for 50+ item galleries. Default:false.dragReorder(boolean): Enables HTML5 drag-to-reorder on grid items. Default:false.backdropTint(boolean): Adapts the overlay color to the dominant color of the current image. Default:false.morphTransition(boolean): Plays a FLIP animation from the grid thumbnail to the lightbox. Default:false.colorPalette(boolean): Shows a five-color extracted palette strip in the lightbox. Default:false.aspectSkeleton(boolean): Usesdata-widthanddata-heightto size loading skeletons. Default:true.video(boolean): Auto-detects MP4, WebM, YouTube, and Vimeo URLs for in-lightbox playback. Default:true.plugins(array): Plugin names or config objects to instantiate on this gallery. Default:null.group(string): Album group name. Navigation traverses all galleries sharing the same group. Also settable viadata-group. Default:''.favorites(boolean): Shows a heart button and persists favorites to localStorage. Default:false.favoritesKey(string): Custom suffix appended to the localStorage key for favorites. Default:''.infoPanel(boolean): Shows a slide-out sidebar with metadata and EXIF data (I key). Default:false.contextMenu(boolean): Adds a right-click context menu to each gallery item. Default:false.shortcutsHelp(boolean): Shows a keyboard shortcuts overlay when the user presses?. Default:true.infiniteScroll(boolean): Auto-loads more items on scroll when aloadMorefunction is set. Default:false.loadMore(function): Callback that receives the current item count and returns an array or Promise resolving to an array of new items. Default:null.editor(boolean): Adds a rotate/flip/export toolbar to the lightbox. Default:false.annotate(boolean): Adds a freehand drawing layer with color picker and export. Default:false.
// Creates a gallery instance from the selected container
const gallery = new NeikiGallery('#project-gallery', {
// Sets the gallery layout
layout: 'mosaic',
// Moves from the last item back to the first item
loop: true,
// Displays the thumbnail rail inside the lightbox
thumbnails: true,
// Enables click-to-zoom inside the lightbox
zoom: true,
// Uses the click position as the zoom origin
contextualZoom: true,
// Shows the browser fullscreen control
fullscreen: true,
// Uses slide transitions between media items
transition: 'slide',
// Follows the operating system theme
theme: 'system',
// Updates the URL hash for direct image links
hashNavigation: true,
// Shows the item counter
counter: true,
// Formats the counter text
counterFormat: '{current} of {total}',
// Animates grid items as they enter the page
stagger: true,
// Enables autoplay with a 5 second interval
slideshow: {
interval: 5000,
pauseOnHover: true,
kenburns: true,
direction: 'forward'
},
// Shows the share control
share: true,
// Builds a tag filter bar from item metadata
filter: true,
// Enables Shift-click multi-select
batchSelect: true,
// Uses smart cropping coordinates from each image
focusPoint: true,
// Uses blur previews before images load
blurhash: true,
// Shows camera metadata for supported JPEG files
exif: true,
// Adds story viewer mode
storyMode: true,
// Adds picture-in-picture mode
pip: true,
// Optimizes very large galleries
virtualScroll: true,
// Lets users reorder gallery items with drag and drop
dragReorder: true,
// Tints the overlay from the active image color
backdropTint: true,
// Animates the thumbnail into the lightbox view
morphTransition: true,
// Shows a 5-color palette strip for the active image
colorPalette: true,
// Reserves layout space from image dimensions
aspectSkeleton: true,
// Detects image, video, YouTube, and Vimeo media
video: true,
// Loads plugin instances
plugins: ['watermark'],
// Links galleries inside the same album group
group: 'case-study-media',
// Shows the favorite heart control
favorites: true,
// Stores favorites under a custom key suffix
favoritesKey: 'case-study',
// Shows the metadata sidebar
infoPanel: true,
// Adds a right-click menu
contextMenu: true,
// Shows the shortcut overlay from the question mark key
shortcutsHelp: true,
// Loads more items near the end of the gallery
infiniteScroll: true,
// Returns new items for infinite scroll
loadMore(currentLength) {
return [
{
src: `media/generated-${currentLength + 1}.jpg`,
thumb: `media/generated-${currentLength + 1}-thumb.jpg`,
caption: `Additional gallery item ${currentLength + 1}`
}
];
},
// Adds rotate and flip tools
editor: true,
// Adds the freehand drawing layer
annotate: true
});4. Use the following HTML Data attributes for declarative setup:
<div data-neiki-gallery
data-layout="mosaic"
data-theme="light"
data-transition="slide"
data-loop="true"
data-stagger="true"
data-slideshow="true"
data-slideshow-interval="4500"
data-slideshow-pause-on-hover="true"
data-slideshow-kenburns="false"
data-counter-format="{current} / {total}"
data-share="true"
data-filter="true"
data-batch-select="true"
data-contextual-zoom="true"
data-story-mode="true"
data-pip="true"
data-exif="true"
data-backdrop-tint="true"
data-morph-transition="true"
data-color-palette="true"
data-drag-reorder="true"
data-virtual-scroll="true"
data-aspect-skeleton="true"
data-group="editorial-shoot"
data-favorites="true"
data-info-panel="true"
data-context-menu="true"
data-shortcuts-help="true"
data-editor="true"
data-annotate="true"
data-video="true">
<!-- Adds tags, dimensions, media type, and focus coordinates -->
<a href="media/editorial-hero.jpg"
data-tags="portrait,studio"
data-size="large"
data-width="1600"
data-height="1100"
data-type="image">
<img src="media/editorial-hero-thumb.jpg"
alt="Editorial portrait with studio lighting"
data-focus="0.45 0.35"
data-blurhash="LEHV6nWB2y...">
</a>
<!-- Opens a local video file in the lightbox -->
<a href="media/product-loop.mp4"
data-tags="video,product"
data-poster="media/product-loop-poster.jpg"
data-type="video">
<img src="media/product-loop-thumb.jpg" alt="Product motion preview">
</a>
</div>5. API methods:
// Open the lightbox at a specific index (zero-based)
photos.open(2);
// Close the lightbox
photos.close();
// Advance to the next image (group-aware across album groups)
photos.next();
// Go back to the previous image (group-aware)
photos.prev();
// Start slideshow autoplay
photos.startSlideshow();
// Stop slideshow and emit stop event
photos.stopSlideshow();
// Pause slideshow — does not emit the stop event
photos.pauseSlideshow();
// Toggle slideshow on/off
photos.toggleSlideshow();
// Filter gallery to a tag; pass null to show all items
photos.filter('landscape');
// Get the array of batch-selected items
const selected = photos.getSelected();
// Clear all batch selections
photos.clearSelection();
// Get current item order as an index array (after drag reorder)
const order = photos.getOrder();
// Toggle favorite for the current lightbox image
photos.toggleFavorite();
// Check if a specific index is favorited
const starred = photos.isFavorite(0);
// Get all favorited image src URLs
const favorites = photos.getFavorites();
// Clear all favorites from localStorage
photos.clearFavorites();
// Toggle the info sidebar — pass true/false to force a state
photos.toggleInfoPanel(true);
// Toggle the keyboard shortcuts overlay
photos.toggleShortcutsHelp();
// Open the in-lightbox image editor (rotate/flip/export)
photos.openEditor();
photos.closeEditor();
// Retrieve the last exported edited image as a Blob
const editedBlob = photos.getEditedBlob();
// Open the freehand annotation layer
photos.openAnnotate();
photos.closeAnnotate();
// Retrieve the annotated image as a Blob
const annotatedBlob = photos.getAnnotatedBlob();
// Print the current (or a specified index) image
photos.print(1);
// Append new items to the gallery at runtime
photos.append([
{ src: 'photos/new-full.jpg', thumb: 'photos/new-thumb.jpg', caption: 'New arrival' }
]);
// Remove an item by index
photos.remove(3);
// Access a plugin instance by name
const watermark = photos.plugin('watermark');
// Register a one-time event listener (auto-removed after first fire)
photos.once('open', (index) => {
console.log('First open at index', index);
});
// Register a persistent event listener
photos.on('change', (index) => {
console.log('Now showing', index);
});
// Remove a specific listener for an event
photos.off('change', myHandler);
// Remove all listeners for an event
photos.off('change');
// Destroy the gallery — removes all listeners and DOM additions
photos.destroy();6. Static utilities:
// Before/after comparison slider on a container element
const compare = NeikiGallery.compare('#diff-container', {
before: 'photos/edit-before.jpg',
after: 'photos/edit-after.jpg',
labelBefore: 'Original',
labelAfter: 'Retouched',
startPosition: 50 // Handle starts at 50% from the left
});
// Move the comparison handle programmatically to 30%
compare.setPosition(30);
// Destroy the comparison slider
compare.destroy();
// Extract a 5-color palette from an image (k-means quantization)
NeikiGallery.extractPalette('photos/hero.jpg', 5, (colors) => {
// colors = [{r, g, b, hex}, ...]
console.log('Palette:', colors);
});
// Extract the single dominant color from an image
NeikiGallery.extractDominantColor('photos/hero.jpg', (color) => {
console.log('Dominant:', color.hex);
});
// Parse EXIF tags from a JPEG binary
NeikiGallery.parseExif('photos/raw-shot.jpg', (tags) => {
// tags = { make, model, iso, fNumber, exposure, focalLength }
console.log('ISO:', tags.iso);
});
// Decode a blurhash string into pixel data
const pixels = NeikiGallery.decodeBlurhash('LEHV6nWB2yk8pyo0adR*.7kCMdnj', 32, 32);
// Detect the media type of a URL string
NeikiGallery.detectMediaType('https://www.youtube.com/watch?v=xyz'); // 'youtube'
NeikiGallery.detectMediaType('clips/reel.mp4'); // 'video'
NeikiGallery.detectMediaType('photos/portrait.jpg'); // 'image'
// Read the current library version
console.log(NeikiGallery.version); // '3.0.0'7. Events:
// Fires when the lightbox opens; receives the image index
photos.on('open', (index) => {
console.log('Opened at index', index);
});
// Fires when the lightbox closes
photos.on('close', () => {
console.log('Lightbox closed');
});
// Fires on every slide change; receives the new index
photos.on('change', (index) => {
console.log('Slide changed to', index);
});
// Fires when the tag filter changes; receives the active tag string
photos.on('filter', (tag) => {
console.log('Active filter:', tag);
});
// Fires when the batch selection changes; receives an array of selected indices
photos.on('select', (indices) => {
console.log('Selected indices:', indices);
});
// Fires when slideshow autoplay starts
photos.on('slideshowStart', () => {
console.log('Slideshow running');
});
// Fires when slideshow autoplay stops
photos.on('slideshowStop', () => {
console.log('Slideshow stopped');
});
// Fires when drag reorder completes; receives the new order array
photos.on('reorder', (order) => {
console.log('New order:', order);
});
// Fires when an image is favorited
photos.on('favorite', ({ index, src }) => {
console.log('Favorited:', src, 'at index', index);
});
// Fires when a favorite is removed
photos.on('unfavorite', ({ index, src }) => {
console.log('Removed favorite:', src);
});
// Fires when the info panel opens
photos.on('infoOpen', () => {
console.log('Info panel visible');
});
// Fires when the info panel closes
photos.on('infoClose', () => {
console.log('Info panel hidden');
});
// Fires when the user prints an image
photos.on('print', ({ index, src }) => {
console.log('Printed index', index, src);
});
// Fires when the editor exports an image; receives Blob and object URL
photos.on('editorExport', ({ blob, url }) => {
console.log('Edited image available at', url);
});
// Fires when the annotation layer exports; receives Blob and object URL
photos.on('annotateExport', ({ blob, url }) => {
console.log('Annotated image available at', url);
});
// Fires when items are appended at runtime; receives the new items array
photos.on('append', (items) => {
console.log(items.length, 'items added');
});
// Fires when an item is removed at runtime; receives the removed index
photos.on('remove', ({ index }) => {
console.log('Removed item at', index);
});
// Fires when picture-in-picture mode activates
photos.on('pipEnter', () => {
console.log('Gallery minimized to PiP window');
});
// Fires when story mode activates
photos.on('storyEnter', () => {
console.log('Story mode opened');
});8. Register a plugin globally before creating any gallery instance that uses it:
// Register a plugin factory — receives the gallery instance and options
NeikiGallery.registerPlugin('analytics', (gallery, opts) => ({
// Runs once after gallery initialization
init() {
console.log('[analytics] ready — sampleRate:', opts.sampleRate);
},
// Runs each time the lightbox opens
open(data) {
track('lightbox_open', { index: data.index });
},
// Runs on every slide change
change(data) {
track('slide_view', { src: data.item.src, index: data.index });
},
// Runs when the lightbox closes
close() {
track('lightbox_close');
},
// Cleanup — runs on gallery.destroy()
destroy() {
console.log('[analytics] teardown');
}
}));
// Pass the plugin name (and any options) to the gallery
const monitored = new NeikiGallery('#featured', {
plugins: [{ name: 'analytics', sampleRate: 1.0 }]
});
// Access the running plugin instance
const analyticsPlugin = monitored.plugin('analytics');
// List all registered plugin names
console.log(NeikiGallery.getRegisteredPlugins()); // ['analytics']
// Unregister a plugin globally
NeikiGallery.unregisterPlugin('analytics');9. Neiki’s Gallery exposes its full visual system as CSS custom properties. Override any variable at the :root level or scope it to a specific gallery container:
:root {
--neiki-columns: 3; /* Grid column count */
--neiki-gap: 12px; /* Gap between tiles */
--neiki-border-radius: 10px; /* Tile corner radius */
--neiki-accent: #f59e0b; /* Accent color — buttons, spinner, pills */
--neiki-overlay-bg: rgba(5, 5, 10, 0.88); /* Lightbox backdrop color */
--neiki-overlay-backdrop: blur(20px) saturate(1.3); /* Backdrop filter */
--neiki-btn-bg: rgba(255, 255, 255, 0.12); /* Toolbar button background */
--neiki-btn-color: #fff; /* Toolbar icon color */
--neiki-caption-color: #f0f0f0; /* Caption text color */
--neiki-spinner-color: var(--neiki-accent); /* Loading spinner color */
--neiki-thumb-size: 56px; /* Thumbnail strip height */
--neiki-zoom-scale: 2.5; /* Zoom level multiplier */
--neiki-stagger-delay: 0.05s; /* Per-item entrance delay */
--neiki-hover-lift: -6px; /* Tile lift on hover */
--neiki-hover-shadow: 0 16px 40px rgba(0, 0, 0, 0.3); /* Tile hover shadow */
--neiki-transition-duration: 0.25s; /* Global transition speed */
}
Switch themes at runtime by setting the data-theme attribute directly on the gallery container:
// Switch a running gallery from dark to light at runtime
document.querySelector('#travel-gallery').dataset.theme = 'light';Alternatives:
- PhotoSwipe: A widely used vanilla JS lightbox with a focus on touch gestures and mobile UX.
- GLightbox: A lightweight JS lightbox with inline, video, and iframe support.
- Fancybox: A feature-rich lightbox library with a commercial license for paid projects.
FAQs:
Q: Can I run multiple independent galleries on the same page?
A: Yes. Each call to new NeikiGallery(selector, options) produces an independent instance with its own state. Use different selectors or IDs for each gallery container.
Q: The lightbox opens but the full-size image is not loading. What should I check?
A: Verify that each <a> tag’s href attribute points to the correct full-size image path. The library reads href for the lightbox source, not the src of the nested <img>.
Q: How do I link two separate galleries so navigation continues across both?
A: Set the same group option on both gallery instances, or add a matching data-group attribute to both containers. Navigation with next() and prev() will then traverse items across all galleries in the group.
Q: Does the slideshow work on mobile without touch conflicts?
A: Yes. The slideshow coexists with swipe navigation. Swiping left or right advances the slide and resets the autoplay interval.
Q: How do I lazy-load more images from a server as the user scrolls?
A: Set infiniteScroll: true and pass a loadMore function that receives the current item count. Return an array of item objects or a Promise that resolves to one. The library calls loadMore automatically when the user reaches the bottom of the grid.
The post Full-featured Vanilla JS Lightbox & Image Gallery – Neiki’s Gallery appeared first on CSS Script.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.
