Full-featured Vanilla JS Lightbox & Image Gallery – Neiki’s Gallery
It supports masonry, grid, mosaic, and horizontal filmstrip layouts for photo-heavy websites, portfolios, documentation pages, and media archives.
data-tags attributes.IntersectionObserver and shimmer placeholders.data-blurhash attribute decoded on load.content-visibility for galleries with 50 or more items.loadMore callback and runtime append / remove API.prefers-reduced-motion support.init, open, change, close, and destroy lifecycle hooks.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 the data-caption text in the lightbox. Default: true.preload (number): Number of adjacent images to preload. Default: 1.lazyLoad (boolean): Lazy loads grid thumbnails via IntersectionObserver. Default: true.stagger (boolean): Plays a staggered fade-in animation when the grid loads. Default: true.slideshow (boolean | object): Enables slideshow autoplay. Pass true or a config object with interval, pauseOnHover, kenburns, and direction. 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 from data-tags values. Default: false.batchSelect (boolean): Enables Shift+click multi-select with checkmark overlays. Default: false.focusPoint (boolean): Reads data-focus on thumbnails to set object-position for smart cropping. Default: true.blurhash (boolean): Decodes data-blurhash attributes 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): Applies content-visibility virtualization 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): Uses data-width and data-height to 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 via data-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 a loadMore function 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'; 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.
May 10, 2026 Imagine if the biggest, most influential businesses in this country came together…
Crimson Desert developer Pearl Abyss has released this week’s update as promised, and it adds…
It took nearly 50 years. WKRP in Cincinnati is no longer just a TV sitcom.…
The Mountain Home Area Chamber of Commerce hosted its 2026 Four-Person Scramble Golf Tournament Friday…
Growing up and spending all of his 44-years in Lead Hill and living on the…
Mountain Home Mayor Hillrey Adams says work is continuing at a rapid pace as the…
This website uses cookies.