Lightbox Q

My javascript >
A lightbox is a very useful and user-immersing method of showing images, by launching a full-screen navigable slideshow when a thumbnail image is clicked on.

There are some geeks who claim they can implement this kind of thing using pure CSS, but really, there's actually no need to rack your brain with such a challenge when javascript can handle the task so easily. There are several open source projects out there (flexcards, fotorama, lightbox2) that can do the job, but I wanted to build it from the bare bones.



The basic lightbox

My starting point was the example set up given at w3c.com, as shown here, the html component, neatened up a little...


Basic lightbox example







So the html layout is like this...

View code
 


The components:


It's very simple as yet, with no play button or mouse scroll button or mobile touch events, that I will add.

For it to run on a web page, it needs styling witn CSS, so that the div and image elements in the html take size, position, colour etc., and of course, some javascript code to tell the browser what to do when the user clicks on the lightbox buttons.

The CSS and javascript files are loaded on the page by inserting lines like these in the html, usually before the lightbox html. The 'defer' tag tells the js to execute only after the page is fully loaded (without which the script should be inserted at the end of the page).

View code
 


The javascript needed to open the 'myModal' elememnt (give it 'display: block' CSS value) and set the image that was clicked on, progress to next/previous, and close the thing again, looks like this...

View code
 

A number of the elements in the html above have 'onClick' commands set to them, so that clicking a thumb opens the lightbox, progresses the show, or switches the cusrrent slide, or closes it.

What it does..


Seems a little technical! But it's standard javascript.

All images are mogrified to webp at 6% to reduce data loading. The thumbnail images are reduced to 200px and stored in a /thumbs dir. I use the following commands to do this (mapped to ranger keys).

View code
 


Then I copy the sub-dir of webp files to my website /images dir.



Interactive lightbox with cross-fade


Here is a worthy attempt to make a an aesthetic user-friendly experience... my Lightbox Q!






You can also see my working lightbox on my other website.



My modifications

I worked on the base model and developed it for a better user experience (latest updates 20/01/25).




Html mods

The html layout looks slightly different now (note, just one thumbnail to click on below). I add the slide-wrapper div to contain the slides, play/pause buttons, and the prev/next areas that cover 100% of the slides, but no lower. The 'modal-content' div contains the 'slide-wrapper' div, the 'caption' div and the 'strip' div containing the thumbs, to create a margin on each side, inside the modal (for click to close).

View code
 

The 'include' tags are liquids used in Jekyll to insert parts into the html. The above lightbox html is saved to a file as a template, located in the _includes dir. Then the page that will display the lightbox calls the template up and supplies the info that the 'include' tags represent by the variables after the dot.

So a typical call for the above template would look like this..

View code
 


The html above is fine if you only have a few images to show, but what if you have 50 or 200? Even 10 would cause some extra work, making sure you have the ten lines in each three places with the correct file names, and for 100s of images this would take hours.

So we automate the process and tell Jekyll to fill in all the correct html for us. In the above we state N=(no. of slides), e.g. N=10... and then we call back N in a for loop, for i in (1..include.N), like this...

View code
 


The liquids capture caption_var and include[caption_var] are necessary here to insert the index no. on each line. The format {{ include.cap{{ i }} }} will not work.

An alternative caption insertion method (good for 100s of images) is to use a plugin that extracts the text from an exif tag on each image file, such as the following...


Exiftag plugin

You can extract the captions from the exif coment tag using a Jekyll plugin for the native exifr... (exiftag, by Beni Buess). It works well in the syntax because it uses the include file brace {%, not double brace {{ (as some other exifr plugins do). I did have to add the lines in the plugin with UTF8, to allow for extracting special characters.

You will need jpg files with the exif data (webp files have no exif). So in the images dir use one more mogrify command, after the comments are written to exif or every time they are updated..

View code
 


The folder jpg~ carries a tilde so that it is not included in the jekyll build, as only the exif data is needed at build time.

Copy the code below (from Github) to a file jekyll-exiftag.rb and save it to the _plugins dir. The plugins saved here are loaded automatically.

Also make sure to have the correct line in the file: require 'exifr/jpeg'.

View code
 


Add the dependency to your Gemfile and run `gem install` to install it.
gem 'exifr'

Or you can install it as a gem, with this line under the jekyll_plugins group in your Gemfile and running `gem install`.
gem 'jekyll-exiftag'

The include tag for the html looks like this (and keep those commas, which are vital)
{% exiftag comment,{{include.folder}}/jpg~,{{i}}.jpg %}

So the html with includes for the plugin looks like this..

View code
 


You will need to write your captions, of course, to the exif comment tags of all the images. Actually, XnviewMP has a tool to do this on the right-click menu, but there's a problem with the comments after the exiftag plugin extracts them into the html, they are written with a | mark on the end.

It works fine with my own script however (I made an Xnview button on the toolbar, but you can easily map it in ranger).

see my exif comment script

Keeping the captions in the exif data should be very safe (unless you wipe the exif data) and you can easily re-organirse or replace images without the captios getting mis-matched.




My style sheets

Note that initial values are for mobile-first awareness, to enable mobile devices to display nicely with small screens.

The values set after @media only screen and (min-device-width: 1200px) override any previous value of same name, for large screens.

I improved the css just now to enable better image size responsiveness (Jan 2026).

CSS for the modal
Using 'z-index: 100' ensures that this element is drawn last..

View code
 


CSS for the slides
Here I needed to use flexbox attributes to allow centering of smaller slides - although now that I'm using 'height: 100%' on images, smaller images will be stretched.

So the mySlides div takes up 100% of slide-wrapper div which takes up 50% (mobile screen) or 80% of the modal area and is positioned at the top.

Fading in/out is achieved using opacity 0 on all slides, which changes to 1 when the class 'fade-in' is added by js, and then back to 0 when the class is removed )so simple!!).

View code
 




CSS for the previous/next areas
The prev and next divs cover the slide div from top to bottom, taking up 50% on left and 50% on the right, and they are drawn on top with z-index: 10. The buttons are contained in these two areas for visual effect, even though clicking anywhere in the area calls the plusSlides fn.

View code
 


CSS for the play/pause buttons
Positioning the buttons with the slide image size takes some work. The buttons are hidden when their class is switched to play0 or pause0 via js. The html starts up with play1 and pause0.

View code
 


CSS for the thumbnail strip
Flexbox is used again, to allow centering of thumbs, both landscape and portrait, which I chop a little to make them nearly squarish.

View code
 


CSS for mobile portrait orientation
A whole different experience is allowed when the mobile is rotated to landscape, with larger slides and the thumbnails strip to the side.

Note that the last '@media only... 1200px' entry was needed below the landscape values to keep large screen display working.

It takes some work to make the appearance look good. Without 'overflow: hidden' on 'slide-wrapper' div the last slides went off the top of screen, for some reason.

View code
 


CSS for the gallery thumbnails
Flexbox again, to allow centering of thumbs, which I chop a little to make them nearly squarish.

View code
 



My javascript

And the javascript code I developed to add all these user-friendly features became quite long. I will break it up in pieces below.


The Modal engine

First of all, the vital part needed to run the lightbox. I use a true/false argument to tell the script if the Modal is open or not, to prevent page scroll when open, and allow it again when closed (see fns below), and to pause when closed (see below).

The fn 'scrollIntoView' is used to create smooth motion of the thumbnail bar, keeping the active thumb at center, except for the ends, which wrap to the other end.

I also switched to a 0-based index (slideIndex = 0) which negates the need for 'slideIndex-1' for the current slide (used when the slide index is 1-n and the div array is 0-n), but the 'currentSlide' fn call must use slide no. -1, in order to find the correct index value (because we number the images 1-n).

Also, to wrap the index from the end back to the start, or from start to end, the line slideIndex = (slideIndex + slides.length) % slides.length; is needed (instead of if (n > slides.length) ... if ( n < 1) ... ), when the index is 0-based.


 


To create the cross-fade of new slide with previous, so that the previous fades out at the same time, I use the 'fade-in' class - add to new slide, remove from previous.

The previous slide must fade out because portrait slides will not cover the whole slide area and clearing the previous slide instantly would look bad.

Notes:



The slideshow timer

The Play and Pause fns are called from the html onClick, attached to the two buttons, which carry the id 'play' and 'pause'. The timer starts using the fn called 'setInterval' and stops with 'clearInterval'. The value 3000 stands for 3seconds, before plusSlides fn calls a new slide. The fn also has to hide the play button when pressed and show the pause button, and then hide the pause button when pressed and show the play button again!

 


Mouse scrolling

Progress through slides using the mouse scroll button. Acomplished using 'event.deltaY'.

 


Keyboard navigation

Keyboard integration, accomplished with 'keyCode' numbers of keys and attaching them to elements in the html that carry onClick events, or else calling functions directly, as with Esc and Spacebar.

 


Prevent unwanted actions

The close event attached to the 'modal' div, that surrounds the slides, is prevented from affecting the child div 'modal-content' with the onClick call to the 'stopClose' fn.
Page body scroll prevented when Modal is opened and mouse is scrolling to progress slides.

 


Touch gestures on mobile screens

Swiping left or right needs some test variables set to decide if the touch was a swipe or was accidental.

 




Multiple lightboxes

The case might arise when you need more than one lightbox on the same page. It won't work however with the html/js as already described, because when the javascript function is called, it will set the class name of the first 'myModal' div it finds on the page or it will progress through all the slides it finds on the page..

To work as expected, each lightbox would need to call the slides within its own div.

You could rename every function in the script and the html, appending a suffix that you can easily replace, like 'L1', 'L2' etc.. But this is a clumsy method, having multiple scripts for the same functions! And errors are bound to spring up when adding more functions later.

Looking at it logically, we should be able to discover the active lightbox modal from the element that is clicked on at the start (if not by finding the one having display: block).

So let's build it so that a click on a gallery thumbnail sets the closest modal div element as a variable, which is then used everywhere in the js while the modal is open.

We can do this using nextElementSibling from the thumbnail container div, which we get using ele.closest('.modal') from the clicked thumbnail.

The onClick call to open the modal needs to pass an argument to the fn to catch the element that is making the call (clicked on), like this..

View code
 
.

The script for multiple lightboxes...

View code
 



And that's it really!


The method I used to build a gallery of multiple image sets, all in one place on the same page, is to use the javascript function 'fetchHtml' which calls html from a file and loads it into a div on the page, replacing the html content of that div.

This way, a single click on a thumbnail or a 'next' button will load a different chunk of html and the javascript on the page then works with the new html, after clicking on a thumbnail in the new html.

I describe the implementation of this idea on my fetch html page.