{% for i in (1..include.N) %}
{% endfor %}
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
{% include exiftag.rb %}
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
{% for i in (1..include.N) %}
{% exiftag comment,{{include.folder}}/jpg~,{{i}}.jpg %}
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
body.stop-scroll { /* stop body scroll -- class added to body via js */
overflow: hidden;
margin-right: 47px;
}
html:has(.modal.modalOpen) .burger,
html:has(.modal.modalOpen) .storynav,
html:has(.modal.modalOpen) .gallery-index {
display: none; /* stop any buttons having display when modal is open */
}
/* The Modal (background) */
.modal {
background-color: rgb(0,0,0,0.8);
cursor: pointer;
position: fixed;
padding-top: 40px;
left: 0; top: 0;
height: 100vh;
width: 100vw;
z-index: 100;
-webkit-tap-highlight-color: transparent;
opacity: 0;
transform: scale(0);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
.modalOpen {
cursor: pointer;
pointer-events: auto;
opacity: 1;
transform: scale(1);
z-index: 100;
}
/* Modal Content */
.modal-content {
position: relative;
width: 95%;
height: 100vh;
padding: 0;
margin: 70px auto;
text-align: center;
}
@media only screen and (min-device-width: 1200px){
.modal {
padding-top: 0px;
}}
/* The Close Button */
.close {
color: #AFAFAF;
cursor:pointer;
position: absolute;
top: 10px;
right: 25px;
font-size: 65px;
font-weight: bold;
}
@media only screen and (min-device-width: 1200px){
.close {
font-size: 35px;
}}
.close:hover,
.close:focus {
color: #AFAFAF;
text-decoration: none;
cursor: pointer;
}
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
.slide-wrapper {
position: relative;
height: 50%;
}
.mySlides {
display: block;
position: absolute;
top: 0;
width: 100%; /* fits in modal-content width */
opacity: 0;
transition: 0.3s ease-out;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.fade-in {
opacity: 1;
transition: 0.3s ease-out;
}
img.slide {
position: relative;
border-radius: 5px;
outline: 3px ridge #AFAFAF;
height: 100%;
width: auto;
}
@media only screen and (min-device-width: 1200px){
.modal-content {
width: 70vw; /* adjust here to set wrapper width -- mySlides width */
height: 90%;
margin: 40px auto;
}
.slide-wrapper {
width: 100%; /* width determined by modal-content */
aspect-ratio: 3 / 2; /* sits exactly on mySlides */
height: auto;
}
.mySlides {
position: absolute;
left:0; top: 0;
height: 100%;
width: 100%;
}
img.slide {
}}
@media only screen and (min-device-width: 1400px){
img.slide{
}}
.slidecaption {
background-color: none;
height: 50px;
padding: 25px 0 15px 0;
color: #AFAFAF;
font-size: 1.2em;
z-index: 50;
}
@media only screen and (min-device-width: 1200px){
.slidecaption {
position: absolute;
bottom: -70px;
height: 40px;
padding: 10px 0;
font-size: 1.2rem;
}}
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
/* Next & previous area */
.prev1,
.next1 {
cursor: pointer;
position: absolute;
top: 0;
bottom:0;
width: 50%;
user-select: none;
-webkit-user-select: none;
z-index: 10;
}
.prev1 { /* Position the "prev area" to the left */
left:0px;
}
.next1 { /* Position the "next area" to the right */
right: 0;
}
/* Next & previous buttons */
.prev-button1, .next-button1 {
cursor: pointer;
position: absolute;
bottom: -350px;
margin-top: -50px;
width: 60px;
height: 50px;
padding: 9px;
color: #AFAFAF;
font-weight: bold;
font-size: 35px;
transition: 0.6s ease;
border-radius: 0 6px 6px 0;
user-select: none;
-webkit-user-select: none;
}
.prev-button1 {
left:0px;
}
/* Position the "next button" to the right */
.next-button1 {
right: 0;
border-radius: 6px 0 0 6px;
}
@media only screen and (min-device-width: 1200px){
.prev-button1, .next-button1 {
top: 50%;
font-size: 20px;
}
/* On hover, add a dark background */
.prev-button1:hover,
.next-button1:hover {
background-color: rgba(0, 0, 0, 0.8);
}}
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
.pause1, .play1 { /* mobile first */
cursor:pointer;
position: absolute;
width: 150px;
height: 50px;
bottom: -340px;
left: 50%;
transform: translate(-50%);
margin: -50px 0 0 -60px;
border-radius: 10%;
color: #AFAFAF;
text-align:center;
line-height: 5px;
padding: 20px 0 0 0;
z-index: 100;
}
.pause1 {font-size: 1.8rem; }
.play1 {font-size: 2.8rem; }
.pause0, .play0 {
display: none;
}
@media only screen and (min-device-width: 1200px){
.pause1, .play1 {
bottom: 65px;
padding: 20px 0 10px 0;
line-height: 15px;
height: 30px;
}
.pause1 {font-size:1.3rem; }
.play1 {font-size:1.9rem; }
.pause1:hover, .play1:hover {
background-color: rgba(0, 0, 0, 0.6);
box-shadow: 1px 1px 3px #999999;
transition: 0.4s ease;
}}
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
.strip { /* mobile first */
display: flex;
background-color: rgb(0,0,0,0.8);
flex-wrap: nowrap; /* how to allow overflow y ??? */
max-height: 150px;
max-width: 95vw;
border-radius: 5px;
margin: auto; /* center strip */
margin-top: 60px;
border: 3px ridge #AFAFAF;
overflow-x: auto; /* allow scrolling */
overflow-y: hidden;
white-space: nowrap;
text-align: center;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.strip::-webkit-scrollbar {
display: none; /* Chrome */
}
.thumb {
display: flex;
width: 215px;
height: 210px;
border-radius: 5px;
overflow: hidden; /* allow thumbs round corners */
margin-right: 20px;
}
.thumb img {
width: 100%;
height: 100%;
object-fit: cover; /* Fills container, crops image to maintain aspect ratio */
object-position: center;
}
@media only screen and (min-device-width: 1200px){
.strip {
max-width: 80vw; /* responsive - shrinks with small screen */
max-height: 135px;
margin-top: 10px;
position: absolute;
bottom: 4px;
}
.thumb {
display: flex;
width: 135px;
height: 101px;
margin-right: 5px;
opacity: 0.7;
}
.active, .thumb:hover {
opacity: 1;
transition: 0.3s;
outline: 0px ridge #AFAFAF;
}}
.thumb img {
width: 100%;
height: 100%;
object-fit: cover; /* Fills container, crops image to maintain aspect ratio */
object-position: center;
}
CSS for alternative thumbnail strip layout (at left edge of modal)
View code
@media only screen and (min-device-width: 1200px){
.strip {
display: block;
position: fixed;
top: 1vh;
left: 20px;
height: 70vh;
width: 138px;
overflow-x: hidden;
overflow-y: auto; /* allow scrolling */
gap: 5px;
}
.thumb {
display: flex;
min-width: 133px;
height: 110px;
margin: 12px 0;
opacity: 0.7;
}
.active,
.thumb:hover {
opacity: 1;
transition: 0.3s;
outline: 0px ridge #AFAFAF;
}}
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
@media only screen and (max-device-width: 1200px) and (orientation: landscape){
.modal {
height: 100vh;
padding-top: 2px;
}
.close {
position: absolute;
top: 50px;
left: 2px;
width: 35px;
height: 40px;
font-size: 35px;
z-index: 40;
}
.slide-wrapper {
position: relative;
height: 99%;
width: auto;
right: 0%;
margin-left: 170px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center; /* vertical center for images under 800px high -apply diplay:flex to parent div */
}
img.slide{
margin: 6px;
}
.prev-button1, .next-button1 {
top: 55%;
}
.strip {
flex-wrap: wrap;
position: absolute;
top: -50px;
left: 20px;
height: 400px;
max-height: 400px;
min-width: 150px;
max-width: 150px;
overflow-x: hidden;
overflow-y: auto;
gap: 5px;
}
.thumb {
width: 160px;
height: 150px;
}
.pause1, .play1 {
width: 50px;
height: 50px;
bottom: -5px;
left: 56px;
color: #797373;
}}
CSS for the gallery thumbnails
Flexbox again, to allow centering of thumbs, which I chop a little to make them nearly squarish.
View code
img.launcher {
opacity: 0.9;
border-radius: 5px;
}
.launcher:hover {
outline: 3px ridge #AFAFAF;
cursor:pointer;
border-radius: 5px;
opacity: 1;
}
.gallerywrapper { /* Gallery page */
position: relative;
width: 90%;
margin-left: auto;
margin-right: auto;
padding: 180px 60px 60px 60px;
min-height: 900px;
}
@media only screen and (min-device-width: 768px){
.gallerywrapper {
width: 80%;
height: auto;
padding: 100px 60px 60px 60px;
}}
.gallery-row { /* Gallery content page tiles area */
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
}
.gallery-col {
padding: 0px;
}
.gallery-col p {
text-align: center;
}
.gallery-col img {
width: 380px;
margin-bottom: 20px;
}
@media only screen and (min-device-width: 1200px){
.gallery-row {
width: 95%;
margin: auto;
}
.gallery-col img {
height: 133px;
width: auto;
margin-bottom: 15px;
}}
@media only screen and (max-device-width: 1200px) and (orientation: landscape){
.gallery-col img {
height: 133px;
width: auto;
margin-bottom: 15px;
}}
.thumbs-list { /* thumbs sub-page */
display: flex;
flex-wrap: wrap;
justify-content: center;
gap:20px;
}
.thumbs-item {
display: flex;
justify-content: center;
align-items: center;
width: 180px;
height: 160px;
opacity: 0.9;
border-radius: 5px;
margin-bottom: 15px;
overflow: hidden; /* allow thumbs round corners */
}
.thumbs-item:hover {
outline: 3px ridge #AFAFAF;
cursor: pointer;
border-radius: 5px;
opacity: 1;
}
.thumbs-item img {
width: 100%;
height: 100%;
object-fit: cover; /* Fills container, crops image to maintain aspect ratio */
object-position: center;
}
.activeitem {
outline: 6px ridge #F08181;
}
@media only screen and (min-device-width: 1200px){
.thumbs-list {
width: 100%;
margin: auto;
gap:10px;
}}
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.
modalClosed = true;
var prevIndex = null;
slideIndex = 0;
const modal = document.getElementById("myModal");
// Open the Modal
function openModal() {
function openModal(thumb) {
modal.classList.add("modalOpen");
stopscroll();
modalClosed = false;
}
// Close the Modal
function closeModal() {
modal.classList.remove("modalOpen");
Pause();
resumescroll();
modalClosed = true;
}
// Next/previous controls
function plusSlides(n) {
prevIndex = slideIndex; // update prevIndex
showSlides(slideIndex += n);
}
// Thumbnail image controls
function currentSlide(n) {
if (n === slideIndex) { return }; // if index same as last time
prevIndex = slideIndex; // update prevIndex
showSlides(slideIndex = n);
}
function showSlides(n) {
var i;
var slides = document.getElementsByClassName("mySlides");
var thumbitems = document.getElementsByClassName("thumbs-item");
var thumbs = document.getElementsByClassName("thumb");
slideIndex = (slideIndex + slides.length) % slides.length;
for (i = 0; i < thumbitems.length; i++) { // highlight active thumb in list
thumbitems[i].className = thumbitems[i].className.replace(" activeitem", "");
thumbitems[slideIndex].classList.add("activeitem");
}
for (i = 0; i < thumbs.length; i++) { // highlight active thumb in strip
thumbs[i].className = thumbs[i].className.replace(" active", "");
thumbs[slideIndex].classList.add("active");
}
// Auto scroll active thumb to center of slider
thumbs[slideIndex].scrollIntoView({
behavior: "smooth", inline: "center"})
slides[slideIndex].classList.add("fade-in");
slides[slideIndex].style.zIndex = "10";
slides[prevIndex].classList.remove("fade-in");
setTimeout((index) => {
slides[index].style.zIndex = "0";
}, 200, slideIndex);
}
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:
'prevIndex = null' this variable is first set to 'null' so that it can be updated
'prevIndex = slideIndex' the variable is updated before the slide index changes (eg. next slide, or rnadom slide). It therefore holds the value of the index (slide no.) before a new slide, i.e. the prevous slide
'setTimeout((index)' tells the script to pause for 200ms, enough for the previous slide to fade out, and then the current slide drops to 'z-index: 0' after starting at '10' to make sure it is displayed on top of the previous (in case of lower slides, i.e. going backwards)
The 'index' returns the value that is held in the closure, after the delay value, i.e. 200, slideIndex) to catch the correct slide.
A vital line is the catch 'if (n === slideIndex) { return };' because if the same slide is called by clicking a thumbhail, the 'prevIndex' will still be on that slide and the slide will inevitably fade out instantly causing a blank modal.
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!
// SLIDESHOW
let timerId
var Timer = false
function startTimer() { timerId = setInterval("plusSlides(1)", 3000); }
function hideplay() { document.querySelector("#play").className = "play0"; }
function showpause() { document.querySelector("#pause").className = "pause1"; }
function Play(event) {
startTimer();
hideplay();
showpause();
Timer = true;
}
function stopTimer() { clearInterval(timerId);}
function hidepause() { document.querySelector("#pause").className = "pause0"; }
function showplay() { document.querySelector("#play").className = "play1"; }
function Pause(event) {
if (Timer == true) {
stopTimer();
hidepause();
showplay();
Timer = false;
} else ;}
function playToggle(event) {
if (Timer == false) { Play(); }
else { Pause(); };
}
Mouse scrolling
Progress through slides using the mouse scroll button. Acomplished using 'event.deltaY'.
// PREV, NEXT WITH SCROLL WHEEL
function wheel() {
if (event.deltaY > 0) { plusSlides(1)}
// Scroll wheel down
else if (event.deltaY < 0) {plusSlides(-1)}
// Scroll wheel up
}
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.
// KEYBOARD NAVIGATION (PREV, NEXT, AND ESCAPE BUTTONS)
document.onkeydown = function (e) {
e = e || window.event;
if (e.keyCode == 37) {document.querySelector('.prev').click();}
if (e.keyCode == 39) {document.querySelector('.next').click();}
if (e.keyCode == 27) {
if (!modalClosed) {closeModal();}
else {document.querySelector('#back-button').click();}
}
else if (e.keyCode == 32) {playToggle();}
}
// TAB KEY & ENTER SELECT THUMB
document.addEventListener('keydown', function(event) {
// Check if the Enter key is pressed
if (event.keyCode === 13) {
// Get the currently focused element
var focusedElement = document.activeElement;
// Trigger a click event on the focused element
if (focusedElement.click) {
focusedElement.click();
}
}
});
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.
// CLICK ON SLIDE DOES NOT CLOSE LB)
function stopClose(event) {
event.stopPropagation();
}
// PREVENTING BODY SCROLL
function stopscroll() { document.querySelector("body").classList.add("stop-scroll"); }
function resumescroll() { document.querySelector("body").classList.remove("stop-scroll"); }
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.
// SWIPE (PREV, NEXT)
var minHorizontalMove = 30; // adjust to your liking
var maxVerticalMove = 30; // adjust to your liking
var withinMs = 1000; // adjust to your liking
// Initialize variables
var startXPos;
var startYPos;
var startTime;
// Touch start event handler
document.addEventListener('touchstart', (e) => {
startXPos = event.touches[0].pageX;
startYPos = event.touches[0].pageY;
startTime = new Date();
})
// Touch end event handler
document.addEventListener('touchend', (e) => {
var endXPos = event.changedTouches[0].pageX;
var endYPos = event.changedTouches[0].pageY;
var endTime = new Date();
var moveX = endXPos - startXPos;
var moveY = endYPos - startYPos;
var elapsedTime = endTime - startTime;
if (Math.abs(moveX) > minHorizontalMove && Math.abs(moveY) < maxVerticalMove && elapsedTime < withinMs) {
if (moveX < 0) {
// Swipe left detected
console.log("Swipe left");
// Call your left-swipe function here
plusSlides(1);
} else {
// Swipe right detected
console.log("Swipe right");
// Call your right-swipe function here
plusSlides(-1);
}
}
})
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...
modalClosed = true;
var modal = null;
var prevIndex = null;
slideIndex = 0;
// Open the Modal closest to the thumb clicked on
function openModal(n, thumb) {
modal = thumb.closest('.thumbs-list').nextElementSibling;
modal.classList.add("modalOpen");
showSlides(slideIndex = n);
stopscroll();
modalClosed = false;
}
// Close the Modal
function closeModal() {
Pause();
modal.classList.remove("modalOpen");
resumescroll();
modalClosed = true;
}
// Next/previous controls
function plusSlides(n) {
prevIndex = slideIndex; // update prevIndex
slideIndex = slideIndex + n;
showSlides(slideIndex);
}
// Thumbnail image controls
function currentSlide(n) {
if (n === slideIndex) { return }; // if index same as last time
prevIndex = slideIndex; // update prevIndex
showSlides(slideIndex = n);
}
function showSlides(n) {
var i;
var slides = modal.querySelectorAll('.mySlides');
var thumbitems = document.getElementsByClassName("thumbs-item");
var thumbs = document.getElementsByClassName("thumb");
slideIndex = (slideIndex + slides.length) % slides.length;
for (i = 0; i < thumbitems.length; i++) { // highlight active thumb in list
thumbitems[i].className = thumbitems[i].className.replace(" activeitem", "");
thumbitems[slideIndex].classList.add("activeitem");
}
for (i = 0; i < thumbs.length; i++) { // highlight active thumb in strip
thumbs[i].className = thumbs[i].className.replace(" active", "");
thumbs[slideIndex].classList.add("active");
}
// Auto scroll active thumb to center of slider
thumbs[slideIndex].scrollIntoView({
behavior: "smooth", inline: "center"})
slides[slideIndex].classList.add("fade-in");
slides[slideIndex].style.zIndex = "10";
slides[prevIndex].classList.remove("fade-in");
setTimeout((index) => {
slides[index].style.zIndex = "0";
}, 200, slideIndex);
}
'modal = null' initializes the variable, so it can be updated each time a new modal opens.
'openModal(n, thumb)' catches the thumbnail slide no. and the element that clicked, as 'thumb', which is used to locate the 'thumbs-list' div, which in turn is used to locate the 'modal' div.
'slides = modal.querySelectorAll('.mySlides')' - the 'showSlides' fn creates the index of slides using this variable 'modal', to gather them only from the open modal.
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).
modalClosed = true;
const modal = document.getElementById("myModal");
To solve this, put the set constant line inside each function, like this...
// Open the Modal
function openModal() {
const modal = document.getElementById("myModal");
modal.style.display = 'block';
stopscroll();
modalClosed = false;
}
// Close the Modal
function closeModal() {
Pause();
const modal = document.getElementById("myModal");
modal.style.display = "none";
resumescroll();
modalClosed = true;
}
Lightbox Animation
The css needed for the open/close animation of the modal...
View code
.modal {
background-color: rgb(0,0,0,0.8);
position: fixed;
padding-top: 40px;
left: 0;
top: 0;
width: 100vw;
height: 95vh;
z-index: 100;
-webkit-tap-highlight-color: transparent;
opacity: 0;
transform: scale(0.1);
transition: opacity 0.3s ease-out, transform 0.3s ease;
pointer-events: none;
}
.modalOpen {
cursor: pointer;
pointer-events: auto;
opacity: 1;
transform: scale(1);
}
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..
function Slider(width) {
const height = width / 1.1; // Maintain 1.1:1 aspect ratio
const sizeStr = `${width}px`;
const heightStr = `${height}px`;
const imageBoxes = document.getElementsByClassName("thumbs-item");
for (let i = 0; i < imageBoxes.length; i++) {
imageBoxes[i].style.width = sizeStr;
imageBoxes[i].style.height = heightStr;
}
}
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
.thumbs-item img {
width: 100%;
height: 100%;
object-fit: cover; /* Fills container, crops image to maintain aspect ratio */
object-position: center;
}
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
.size-slider {
accent-color: #4CAF50; /* Green thumb */
width: 80%;
margin: 20px 0;
float: right;
right: 0;
margin:0 40px 20px;
}
.size-slider::-webkit-slider-runnable-track {
background: #ddd;
height: 8px;
border-radius: 4px;
}
.size-slider::-moz-range-track {
background: #ddd;
height: 8px;
border-radius: 4px;
}
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
.modal {
background-color: rgb(0,0,0,0.8);
cursor: pointer;
position: fixed;
padding-top: 40px;
left: 0; top: 0;
width: 100vw;
height: 95vh;
z-index: 100;
-webkit-tap-highlight-color: transparent;
opacity: 0;
transform: scale(0.1);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
z-index: 1;
}
.modal:has(:target) {
cursor: pointer;
opacity: 1;
transform: scale(1);
z-index: 100;
}
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
.backdrop {
cursor: pointer;
position: fixed;
inset: 0;
z-index: 1;
}
.backdrop, .slides-box, .mySlide {
pointer-events: auto;
}
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
.slides-box {
position: relative;
width: 98%;
aspect-ratio: 3 / 2;
margin-top: 60px;
display: flex;
z-index: 10;
gap: 5px;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none;
}
.slides-box::-webkit-scrollbar {
display: none; /* Chrome */
}
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.