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

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.


Discover more from RSS Feeds Cloud

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading