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!


Size
×




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

As of June 26, I think I have found the best solution to the modal layout...

First of all, I have a content div, that sets the area on the page using margins. Then the wrapper element sits inside the content div with 'width: 100%' and 'aspect-ratio: 3 / 2' to control its height, and it holds the slide container with the next/prev overlay elements.

The mySlides div has 'position: absolute' and takes up 100% of the wrapper. The images then take up 100% height of the slide box, with width 'auto', which keeps portraits tall.

When the page size changes horizontally, the wrapper adjusts with the container and its height keeps aspect with the changing width, the slides box follows this change of size and thus the images are responsive to any page size.

Note that if the modal container does not use 'display: flex', the wrapper inside it will need set width/height and 'margin: auto'.

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 alternative thumbnail strip layout (at left edge of modal)
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.


 


I added the fn to highlight the active thumb in the thumbs list, so that when you close the lightbox you can see which image in the list you had been viewing.

Something to help the design is an open/close animation of the full-screen modal. To accomplish this, we can't really use 'display: none' and 'display: block', as when you close the modal it will be instantly removed with no time to fade out. So we need to go about it adding and removing a class with 0 or 1 opacity.

Note the lines ading and removing the 'modalOpen' class above - see below for the css needed to control the crossfade.

To create the cross-fade of slides, so that the previous fades out while new slide fades in, I use the 'fade-in' class - simply 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 about the js above:



Note about csoss-fading divs with fetched html: if you use the technique I describe on my fetch html page, which uses two divs that are cells of a grid (using absolute positioned divs causes chaos with the page height and footer), the js using the myModal id will only find the html in one of the grid cel and not the other - sesulting in no modal in some albums.

To solve this, use the multi modal js as shown further below, which finds the modal which is closest to the thumb clicked on.


The slideshow timer

The Play and Pause buttons call a fn 'playToggle' via inline html onClick, which carry the id 'play' and 'pause' to show/hide them. 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...

 



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 element on the page, replacing the html content of that div.

This way, a single click on a thumbnail or a 'next' or 'previous' or 'back' 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 after the html is fetched.

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


Note that if you set the 'modal' constant at the top of the js (as below), which does a 'document.getElementById' scan for the element with 'myModal' id, and you are fetching new html content and changing this element, the js returns an error when clicking on a thumb (and the modal fails to show).
 


To solve this, put the set constant line inside each function, like this...

 



Lightbox Animation

The css needed for the open/close animation of the modal...
View code
 


Using 'transform: scale(0.1)' reduces the modal element to almost 0 size and into the center of the screen by default - so you could have it emerge/return to a certain point other than center. Note that the transition line is only needed once, under the ',modal' class.


Thumbnail slider

To dynamically change the size of the thumbnails in the gallery, use a 'input type' div with your range and start value, which triggers a js function.

The benifit of this is, there might be a user who wants to view the all thumbs without scrolling down the page.
View code
 


The js function targets the thumnail image container element with the value sent by the slider, like this..

 

So I take the value as the thumb container width and then divide that by 1.1 to get the height, as my thumbs are rather square.

To enable the thumb images to stretch with the container, make sure they have wd/ht set to 100% and use 'object-fit: cover' in a flex container., like this..
View code
 


And also, if in your css you have a max width set for the thumbs container, then the thumbs will not go past that size and it will look funny as the slider apparently does nothing past, say, 60%.

You also really want to set your css thumb container width to the same value as the start value of the slider, because the page loads with the css value, and then interaction with the slider will set new values afterward - and css width @ 200px and slider @ 100px will make a bad jump. So I set them both to 110px (ht 100px).

Some css to control the slider's position/appearance...
View code
 

Pure css lightbox

It is almost possiblue to create the same lightbox experience without using javascript, thanks to a CSS trick with the ':has(:state)' detector on the modal. The main differences are no clicking on slides for next, no cross-fading, no thumbnail strip which auto scrolls, no timer etc.

How it works is each thumb in the list carries an anchor tag that targets a slide of same no. in the modal, which auto scrolls to the slide instantly, The modal is hidden initially but with the CSS detector, that turns true when modal contains an element with a target state, it transforms into visibility.

The modal contains a snap-scroll container with the slides that can be scrolled with mouse wheel. Around that is a 'backdrop' area which is actually an anchor tag that targets '#', which is page top, and as no slide has this target the detector turns false and modal closes.

The thumbnails html layout...
View code
 


The modal html...
View code
 


The CSS detector!
View code
 


Note that the modal, while hidden, must be scaled to 0.1, and not 0, so that the browser can find the anchor targets on the slides. If they are not drawn at all on the page then the anchors cannot be found and the result is a thumb click calls the next slide from the one before.

The backdrop
View code
 


Also note, the snap-scroll container and the slides need 'pointer-events: auto' so that clicking on a slide does not go through to the backdrop and close the modal. The backdrop also needs it so that the thumbnail list doesn't scroll down underneath while modal is open.

And the snap-scroll container of course....
View code
 


This scrolls on the y-axis, up-down, for the mouse wheel, but for mobile screens you could set it for the x-axis, for swiping left-right.