Categories: CSSScriptWeb Design

Full-featured Vanilla JS Lightbox & Image Gallery – Neiki’s Gallery

Neiki’s Gallery is a vanilla JavaScript image gallery and lightbox library that displays responsive image collections, videos, embeds, and full-screen media overlays.

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-tags attributes.
  • 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 IntersectionObserver and shimmer placeholders.
  • Blurhash placeholder support via a data-blurhash attribute 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-visibility for 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 loadMore callback and runtime append / remove API.
  • 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-motion support.
  • Rich event system covering open, close, change, filter, select, slideshow, favorites, editor export, and more.
  • Plugin system with init, open, change, close, and destroy lifecycle 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-gallery initialize 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';

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.

rssfeeds-admin

Share
Published by
rssfeeds-admin

Recent Posts

Jodi’s Journal: The rest of the story behind Forward Sioux Falls

May 10, 2026 Imagine if the biggest, most influential businesses in this country came together…

18 minutes ago

Crimson Desert Adds Surprise Claw Machine Mini-Game and Lets Pet Dogs Attack Enemies as Part of Update 1.06.00

Crimson Desert developer Pearl Abyss has released this week’s update as promised, and it adds…

23 minutes ago

Nearly 50 Years Later, WKRP in Cincinnati Becomes a Real Radio Station

It took nearly 50 years. WKRP in Cincinnati is no longer just a TV sitcom.…

28 minutes ago

Record turnout, beautiful weather highlight Friday’s Chamber Golf Tournament at Big Creek

The Mountain Home Area Chamber of Commerce hosted its 2026 Four-Person Scramble Golf Tournament Friday…

36 minutes ago

Lead Hill man competes on Netflix reality show “Million Dollar Secret”

Growing up and spending all of his 44-years in Lead Hill and living on the…

37 minutes ago

MH Mayor Adams gives update on community center progress

Mountain Home Mayor Hillrey Adams says work is continuing at a rapid pace as the…

38 minutes ago

This website uses cookies.