A golden sculpture

How to add a gallery to basicLightbox

| Programming   Javascript   Webdesign  

Recently, I was in need for a lightweight lightbox plugin. I stumbeled upon basicLightbox and loved its simplicity. Unfortunately, the plugin doesn't have a gallery functionality built in. In this article, I will show you step by step, how I achieved it anyways.

As mentioned above, my solution for a gallery expansion is based upon Tobias Reichs javascript plugin "basicLightbox". I chose basicLightbox because, with just 4KB of un-gzipped filesize, it is the most lightweight plugin I could find. Also, this plugin is pure javascript without dependencies or libraries attached, which is something I highly value. For an example of the gallery functionality, click the images to the right of this text.

Setup

To setup basicLightbox, you just have to include two files into your project:

<link href="/css/basicLightbox.min.scss" rel="stylesheet">  
<script src="/js/basicLightbox.min.js"></script> 

After that, you're ready to go. The peculiar thing about basicLightbox is, that it doesn't come with automatic binding to any elements or other fancy functionalities. You can basically only tell it: "please make a lightbox with some content I provide and then open it". In code, it would look something like this:

import * as basicLightbox from 'basiclightbox'

const instance = basicLightbox.create(
  `//my amazing content`, 
  { options : 'amazing'}
);

instance.show();

While this makes the plugin incredibly versatile, it also means: if you want to be able to click on an image and the lightbox is supposed to pop up, you gotta code that yourself. That (and some arrows to use the lightbox as a slideshow) is what we are going to do in the following.

What we need

Lets think about our components first. We need

  1. The lightbox plugin *duh*
  2. Images or thumbnails to click on
  3. A list of said images
  4. A script to open the lightbox when a thumbnail is clicked
  5. A script that shows which content to use
  6. Content to put into the lightbox (e.g. the large version of an image)
  7. Arrows inside the lightbox in order to jump to the previous/next content
  8. A script that handles the arrows functionality.

Weiter up, I explained how to include the plugin. Next step would be the images. This example is based on Kirby as a content management system, but it works with other solutions aswell. It is important, however, that you are able to manipulate the image output directly. Meaning: if a tag group like

<figure>
    <img src="amazingImage.jpg" alt="I always forget to write alt">
    <figcaption>An amazing image</figcaption>
</figure

is generated automatically, without you being able to manipulate some attributes, you probably have to look for a different approach. 😔

Thumbnails and data

For everybody else: In the spirit of unobtrusive javascript, we are going to enhance our figure tag with data-attributes. These help to pass data to js-eventlisteners and can be named after the data they contain. In our case, we want to pass three things:

  1. The url of the bigger image
  2. The caption of the bigger image (could be the same as figcaption, but maybe you want something different in there)
  3. An index
    Also, your figure-tag should have a class name. That will come in handy later.

There are several ways to pass these values into the attributes. Chances are, that you will encounter some kind of foreach-loop. Here is an exaple of how I solved it in Kirby (wich runs on php):

<?php 
  $gallery_counter = 0;
  foreach ($images as $image) : ?>
  <figure class="gallery" 
        data-originalurl="<?= $image->url() ?>" 
        data-caption="<?=$image->caption()?>" 
        data-imgageindex="<?= $gallery_counter ?>">

        <img class="gallery-image" 
            src="<?= $image->thumb()->url() ?>" 
            alt="<?= $image->alt() ?>">

        <figcaption> <?= $caption ?></figcaption>

    </figure>
<?php gallery_counter++; ?>
<?php endforeach; ?>

In the browsers source code, our figures should look something like this now:

<figure data-originalurl="https://some.url" data-caption="another caption" data-imgageindex="0">
    <img src="amazingImage.jpg" alt="I always forget to write alt">
    <figcaption>An amazing image</figcaption>
</figure>

List of images

Since we now have the data-enhanced figures on our site, let's talk about putting their data into a list aswell. For that, we will use a 2d-array (basically an array that stores arrays). Each index of our array stands for one image and stores its url, caption and index (although you could probably skip the latter, since its value is identical with the index of the outer array. But for simplicities sake, were going to leave it in). Theoretically, its structure is going to be like this:

let galleryImages =[
[image1url, image1caption, image1index],
[image2url, image2caption, image2index],
[image3url, image3caption, image3index]
];

There is two approaches to get there: either fill it up via php with the help of the foreach-loop mentioned above or do it after the site has loaded via javascript.

Here is the php-solution:

<script>
  const galleryList = [
    <?php foreach ($images as $image) : ?>
      ['<?= $image->url() ?>',
      '<?=$image->caption()?>',
       <?= $gallery_counter ?>],
    <?php endforeach;?>
  ];
</script>

Here is the js-solution. Make sure to put/load it at the end of the site. As you can see, the data-attributes of our figures can be accessed via .dataset, followed by our own attribute name.

let galleryList = [];
let i = 0;
document.querySelectorAll('.gallery').forEach((item) => {
    newlist[i] = [
      item.dataset.originalurl,
      item.dataset.caption,
      item.dataset.imgageindex,
    ]
    i++;
});

Either way works. For reduced redundancy, you might also generate the list first and render the figures out of its data. However, since I am working with automatically generated thumbnails here (seen in the first foreach-loop at image()->thumb()-url()), the way shown was a more convenient approach.

The Lightbox (🎉)

Now that we have everything setup, we can finally open the lightbox.
First of all, we need an instance of the lightbox. In this example, we'll just create an "empty" instance upon pageload, but you could also bind the creation to the first click on an image or something like this. It's up to you.

// Creation of Lightbox instance, but dont show it yet

const instance = basicLightbox.create('', {
    className: 'lightbox',
});
// In order to change the content later, we need the DOM-element of the instance:
const elem = instance.element();

As you can see, the first argument in basicLightbox.create() is an empty string. This is where the content will go later. The second argument cares about the options of our instance. In this case, well just give the class name "lightbox" to it. In order to modify the lightbox later on, we need to get a hold of the DOM-element/node that belongs to our instance. Thats what .element() is for.

Next up is the event listener. After all, the lightbox should appear after we click an image. Here is how we do that:

//select all elements with the class .gallery
document.querySelectorAll('.gallery').forEach((item) => {
//add an eventlistener to each one that triggers the appearance of the lightbox on click
  item.addEventListener('click', (event) => {
    instance.show(); // this shows the lightbox
  });
});

If you clicked on an image right now, you'd feel an odd mix between joy and disappointment. The lightbox showed, but there was nothing in it. Let's fix that. We can tell our const elemwhat html to show, by simply accessing it via good old .innerHTML. Since we want the content to be a bigger version of our thumbnail figure, we can reuse that code and put it into a string like so:

let htmlValue =`
<figure id="lightboxContent" class="lightbox-figure">
  <img class="lightbox-img" src="">
  <figcaption></figcaption>
</figure>
`

As you can see, there is one big problem: we can't access the content of src or <figcaption>. Thatswhy, we make htmlValuea function that returnsa string with dynamically changed parts:

function lightboxHtml(imgUrl, imgcaption) {
  let htmlValue = `
    <figure id="lightboxContent" class="lightbox-figure">
    <img class="lightbox-img" src="${imgUrl}">
    <figcaption >${imgcaption}</figcaption>
    </figure>
    `;
  return htmlValue;
}

If you haven't noticed yet, this is the part where it all comes together: We wrap everything we have in another function, update the event handlers to pass the data-attributes of our thumbnail-figures as arguments and voilà: the lightbox shows what it is supposed to show. Here is the complete code:


function lightboxHtml(imgUrl, imgcaption) {
  let htmlValue = `
    <figure id="lightboxContent" class="lightbox-figure">
    <img class="lightbox-img" src="${imgUrl}"> //inject url
    <figcaption >${imgcaption}</figcaption> //inject caption
    </figure>
    `;
  return htmlValue;
}

function makeLightbox(url, caption, index) {
  elem.innerHTML = lightboxHtml(url, caption); // update content of lightbox according to the result of lightboxHTML()
  instance.show(); //show the lightbox
}

document.querySelectorAll('.gallery').forEach((item) => {
  item.addEventListener('click', (event) => {
    makeLightbox( //pass data-attributes of figure
      item.dataset.originalurl,
      item.dataset.caption,
      item.dataset.imgageindex,
    );
  });
});

Our lightbox is working! 🥳 You might have wondered, though, why we pass the index-argument into our makeLightbox-function. Well, let's look at our list of needs in order to determine what is left to do and it should become obvious:

  1. The lightbox plugin
  2. Images or thumbnails to click on
  3. A list of said images
  4. A script to open the lightbox when a thumbnail is clicked
  5. A script that shows which content to use
  6. Content to put into the lightbox
  7. Arrows inside the lightbox in order to jump to the previous/next content
  8. A script that handles the arrows functionality.

Before we dive into the next part, here is some styling for the lightbox that i found useful:

.lightbox-figure {
  display: none;
  max-height: 85vh;
  max-width: 85vw;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s ease;
  figcaption {
    color: #fafafa;
  }
}

.lightbox-img {
  object-fit: cover;
  height: 100%;
  width: 100%;
  max-height: 100%;
  max-width: 85vw;
  max-height: 85vh;
}

Gallery functionality

As it seems, there is only left to put clickable arrows into our lightbox and make it jump to the next (or previous) image. First, let's add the arrows to our lightboxHtml()-function. We will use buttons for that. Since the arrows should link to the previous/next image, well add two more arguments to the function aswell. Those arguments will be injected into the data-next-attribute of the button. Have a look:

function lightboxHtml(imgUrl, imgcaption, prevImg, nextImg) {
  let htmlValue = `
    <button data-next="${prevImg}" class="gallery-arrow gallery-arrow-prev">«</button>
    <figure id="lightboxContent" class="lightbox-figure">
    <img class="lightbox-img" src="${imgUrl}">
    <figcaption>${imgcaption}</figcaption>
    </figure>
    <button data-next="${nextImg}" class="gallery-arrow gallery-arrow-next">»</button>
    `;
  return htmlValue;
}
.gallery-arrow {
  top: 50vh;
  position: absolute;
  border: none;
  background: transparent;
  width: 48px;
  height: 48px;
  fill: white;
  opacity: 0.5;
  transition: all 0.3s ease;

  &-prev {
    left: 6vw;
  }
  &-next {
    right: 6vw;
  }
  &:hover {
    opacity: 0.8;
    transition: all 0.3s ease;
  }
}

Of course, we'll also have to update our makeLightbox()-function now, since it has to pass 4 instead of 2 variables into lightboxHtml(). Basically, we want to compute the previous and next index in relation to our current image. We should keep in mind, that the first index ([0]) is our next index, when the current image is the last one in the array and vice versa. A simple if-statement should do the trick:

function makeLightbox(url, caption, index){
  let nextIndex = index + 1;
  if (nextIndex > galleryList.length - 1){
    nextIndex = 0;
  }
  let prevIndex = index - 1;
  if (prevIndex < 0){
    prevIndex = galleryList.length - 1;
  }
  elem.innerHTML = lightboxHtml(url, caption, nextIndex, prevIndex);
  instance.show();
  initiateArrowEventlisteners();
}

Right now, nothing would happen if we clicked the arrows, because there is no event listener for them yet (although the function is called in the code above). Hence, we need another event listener. This is also the part, where our array galleryList comes in: we can access the data of the next/previous images by simply using the arrow-buttons data-attribute:

//put the listener into a fuction in order to renew it after we clicked an arrow
function initiateArrowEventlisteners(){  
  document.querySelectorAll('.gallery-arrow').forEach((item) => {
    item.addEventListener('click', (event) => {
      makeLightbox(
          galleryList[item.dataset.next][0], //access first index of next image (url)
          galleryList[item.dataset.next][1], //access second index of next image (caption)
          galleryList[item.dataset.next][2] //access third indes of next image (index)
        );
    })
  })
}

There is one last change we should make. Our arrows shouldn't show if there is only one image on the site, right? While an if-else-statement is a totally viable option, let's go for a Conditional (ternary) operator to save some space.

${galleryList.length > 1 ? `<button data-next="${prevImg}" class="gallery-arrow gallery-arrow-prev">«</button>` : ''}

//our lightboxHtml()-function now looks like this:

function lightboxHtml(imgUrl, imgcaption, prevImg, nextImg) {
  let htmlValue = `
    ${galleryList.length > 1 ? `<button data-next="${prevImg}" class="gallery-arrow gallery-arrow-prev">«</button>` : ''}
    <figure id="lightboxContent" class="lightbox-figure">
    <img class="lightbox-img" src="${imgUrl}">
    <figcaption>${imgcaption}</figcaption>
    </figure>
    ${galleryList.length > 1 ? ` <button data-next="${nextImg}" class="gallery-arrow gallery-arrow-next">»</button>`: ''}
    `;
  return htmlValue;
}

And that is pretty much it. Our lightbox gallery is up and running. I hope that this article was helpful to you. How did you like this tutorial? Do you have something to add or some efficiency boosts? Please let me know in the comments if there are any questions or suggestions. Also, feel free to share this article.

Best,
Don

P.S.: Below, you'll find the ready-to-go code. I also added in a little fade-in effect.

//Create Lightbox instance on pageload but dont show it yet
const instance = basicLightbox.create('', {
  className: 'lightbox',
});

const elem = instance.element(); //this is the DOM-element of the instance

//Blueprint for the gallery inside the Lightbox.
function lightboxHtml(imgUrl, imgcaption, prevImg, nextImg) {
  let htmlValue = `
    ${galleryList.length > 1 ? `<button data-next="${prevImg}" class="gallery-arrow gallery-arrow-prev">«</button>` : ''}
    <figure id="lightboxContent" class="lightbox-figure">
    <img class="lightbox-img" src="${imgUrl}">
    <figcaption>${imgcaption}</figcaption>
    </figure>
    ${galleryList.length > 1 ? ` <button data-next="${nextImg}" class="gallery-arrow gallery-arrow-next">»</button>`: ''}
    `;
  return htmlValue;
}

// Fill the instance with content and show it
function makeLightbox(url, caption, index) {
  let nextIndex = index + 1
  if (nextIndex > galleryList.length - 1) {
    nextIndex = 0;
  }
  let prevIndex = index - 1
  if (prevIndex < 0) {
    prevIndex = galleryList.length - 1;
  }
  elem.innerHTML = lightboxHtml(url, caption, nextIndex, prevIndex);
  instance.show();
  animateFadein();
  initiateArrowEventlisteners();
}

function animateFadein() {
  var figure = document.getElementById('lightboxContent');
  figure.style.display = 'block';
  figure.clientHeight;
  figure.style.opacity = 1;
}

// Eventlistener for click events on gallery images/figures
document.querySelectorAll('.gallery').forEach((item) => {
  item.addEventListener('click', (event) => {
    makeLightbox(
      item.dataset.originalurl,
      item.dataset.caption,
      item.dataset.imgageindex,
    );
  });
});

//eventlistener for gallery arrows
function initiateArrowEventlisteners(){  
  document.querySelectorAll('.gallery-arrow').forEach((item) => {
    item.addEventListener('click', (event) => {
      makeLightbox(
          galleryList[item.dataset.next][0],
          galleryList[item.dataset.next][1],
          galleryList[item.dataset.next][2]
        );
    })
  })
}

// fill the galleryList
let galleryList = [];
let i = 0;
document.querySelectorAll('.gallery').forEach((item) => {
    newlist[i] = [
      item.dataset.originalurl,
      item.dataset.caption,
      item.dataset.imgageindex,
    ]
    i++;
});
<!doctype HTML>
<html>
<head>
<title>My superamazing lightbox test</title>
<link href="/css/basicLightbox.min.scss" rel="stylesheet">  
<script src="/js/basicLightbox.min.js"></script> 
</head>
<body>
<figure data-originalurl="https://some1.url" data-caption="caption" data-imgageindex="0">
    <img src="amazingImage1.jpg" alt="I always forget to write alt 1">
    <figcaption>An amazing image 1</figcaption>
</figure>

<figure data-originalurl="https://some2.url" data-caption="another caption" data-imgageindex="1">
    <img src="amazingImage2.jpg" alt="I always forget to write alt 2">
    <figcaption>An amazing image 2</figcaption>
</figure>

<figure data-originalurl="https://some3.url" data-caption="yet another caption" data-imgageindex="2">
    <img src="amazingImage3.jpg" alt="I always forget to write alt 3">
    <figcaption>An amazing image 3</figcaption>
</figure>
</body>
</html>
.lightbox-figure {
  display: none;
  max-height: 85vh;
  max-width: 85vw;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s ease;
  cursor: pointer;

  figcaption{
    color: #fafafa;
  }
}

.lightbox-img {
  object-fit: cover;
  height: 100%;
  width: 100%;
  max-height: 100%;
  max-width: 85vw;
  max-height: 85vh;
}

.gallery-arrow {
  top: 50vh;
  position: absolute;
  border: none;
  background: transparent;
  width: 48px;
  height: 48px;
  fill: white;
  opacity: 0.5;
  transition: all 0.3s ease;

  &-prev {
    left: 6vw;
  }
  &-next {
    right: 6vw;
  }
  &:hover {
    opacity: 0.8;
    transition: all 0.3s ease;
  }
}

Comments

Add a comment