Modal Dialog – Creating Gallery With Modal Preview

Reading Time: 11 minutes

Welcome back for another great tutorial. It’s been a while since we build something in HTML, CSS and JavaScript. For this reason, I prepared a challenge for today … Our goal will be building simple gallery enhanced with sleek preview in custom modal dialog. And, not only that. Our gallery will be featuring implemented grid – from Bootstrap 4 – with custom modal dialog to show larger preview. What’s more, you will be also able to cycle through preview images using either arrow keys or buttons in modal dialog. Are you ready to take on this challenge?

If you like JavaScript, today is your lucky day. The most of your time spent on this tutorial will be focused on JavaScript. On the other hand, in case you are a beginner this tutorial will offer you an interesting insight into more advanced JavaScript. This tutorial will also show you how to start implementing design patterns, namely modular pattern.

You can see the demo on Codepen.

Let’s Make it More Fun – Modal dialog and Constraints

Another thing I want to address before moving to the tutorial is the intention to make the whole tutorial as easy to implement as possible. Meaning, the majority of work should rely on JavaScript, not the user. So, the less steps user has to make in order to make the modal preview work, the better. For this reason, we are going to make this challenge little bit tougher by adding couple more constraints. The first constrain will be that user will not be forced to use some specific class to mark all images in the gallery.

Instead, he will use what ever class he wants and then just use that class as a value for data attribute for the grid div. Next constraint will be that the code for modal dialog can’t be placed straight inside the HTML. The solution? Yes, we are going to create a some kind of modal constructor in JavaScript that will later “insert” prepared modal dialog into the HTML when needed. That’s all for the constrains. Initially, I wanted to add a no jQuery rule, but that will be too hard and make code too long. So, we will pass on this for now.

Note: Images used in this tutorial are from unsplash website and are served via its API. All of these images are distributed under Creative Commons Zero. What this means for you is that you are free to use, copy, modify or distribute them as you wish. You can even use them in your commercial projects!

HTML

The HTML part of this project will compose of one main div that will contain the grid with all images. These images will be nested in groups of three in row divs. I decided to implement nine rows in total to make the gallery little bit more interesting. Every row will contain three columns with single image inside each column.

<!-- Begin div .grid -->
<div id="gallery__grid" class="container gallery__grid" data-element="gallery-item">
 <!-- Begin div .row 1 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/nasa/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/erondu/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/category/people/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 1 -->

 <!-- Begin div .row 2 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/lukasbudimaier/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/category/objects/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/fableandfolk/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 2 -->

 <!-- Begin div .row 3 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/mickeyoneil/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/category/nature/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/joannakosinska/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 3 -->

 <!-- Begin div .row 4 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/leadbt/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/category/technology/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/davideragusa/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 4 -->

 <!-- Begin div .row 5 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/erichuang78910/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/category/buildings/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/elliottengelmann/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 5 -->

 <!-- Begin div .row 6 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/category/nature/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/fritzbielmeier/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/thata7guy/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 6 -->

 <!-- Begin div .row 7 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/pjrvs/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/olliepb/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/category/food/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 7 -->

 <!-- Begin div .row 8 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/jonottosson/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/hideobara/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/sylvain_guiheneuc/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 8 -->

 <!-- Begin div .row 9 -->
 <div class="row">
  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/worthyofelegance/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/category/buildings/400x400" alt="gallery thumbnail">
  </div>

  <div class="col-md-4 gallery__column">
   <img class="gallery-item" src="https://source.unsplash.com/user/_vickyreyes/400x400" alt="gallery thumbnail">
  </div>
 </div><!-- end div .row 9 -->
</div><!-- end div .grid -->

CSS

Do you remember the constrains I introduced in the beginning? Well, as said, the majority of the CSS will be handled by JavaScript. Having said that, don’t try this at home! The best practice for working with CSS (or Sass) is to keep them in stylesheets. You can also use inline or internal styles, but you should avoid this as well. Just keep your styles where they should be – in stylesheet.

/* Resetting font size */
html {
 font-size: 1rem;
}

/**
 * Grid
 */
.gallery__grid .row:nth-child(n+2) {
 /* Adding some margin to create a bit of space under rows */
 margin-top: 1rem;
}
.gallery__column {
 /* In case the image is bigger than the column */
 overflow: hidden;
}

JavaScript

HTML and CSS is in place and now it’s time to move to the most difficult part of this Modal Games tutorial (or project). Before diving in, I want to warn you … It is quite possible that your knowledge of JavaScript, more precisely jQuery, will be put in hard test. Also, don’t be surprised if you will not want to see any line of JavaScript for a while after finishing this tutorial. This kind of reaction is completely normal. So, don’t force yourself into anything. Take a break and let your body and brain rest and recharge. That being said, let’s finally get the JavaScript part in shape.

// Creating self-invoking anonymous function
(function() {
 'use strict';
 var app = {
   // Setting couple variables 
   settings: {
     grid: $('#gallery__grid'),
    modalDialog: $('#modal-dialog'),
    elementClass: $('#gallery__grid').data('element')
   },
   controllers: function() {
    // Run the code only if gallery exists
    if (this.settings.grid.length > 0) {
     // Modal Constructor
     var modalBuilder = '<div id="modal-dialog" class="js-modal-overlay modal__overlay"><section class="js-modal-dialog modal-dialog"><button class="js-modal-close modal__close"><span class="sr-only">Close Modal</span>&Cross;</button><img class="js-modal-dialog__img modal-dialog__img" src="" alt="Gallery thumbnail"><div class="modal__navigation"><a href="#" class="js-modal-prev modal__prev theme-background">&lsaquo;</a><a href="#" class="js-modal-next modal__next theme-background">&rsaquo;</a></div></section></div>';
     // Putting the modal contructor right after the grid container
     $(app.settings.grid).after(modalBuilder);

     // Creating variables we are going to use through the code
     var modalOverlay = $('.js-modal-overlay'),
         modalImage = $('.js-modal-dialog__img'),
         modalCloseBtn = $('.js-modal-close'),
         nextImageAnchor = $('.js-modal-next'),
         prevImageAnchor = $('.js-modal-prev'),
         grid = $('.gallery__grid'),
         imagesArray = grid.find(this.settings.elementClass),
         imagesArrayLength = imagesArray.length;
 
     // Styling the modal dialog
     $('.js-modal-dialog').css({
      'position': 'relative',
      'top': '50%',
      'right': '0',
      'left': '0',
      'transform': 'translateY(-50%)',
      'margin-right': 'auto',
      'margin-left': 'auto',
      'max-width': '25rem',
      'text-align': 'center'
     });
 
     // Styling the modal overlay
     modalOverlay.css({
      'position': 'fixed',
      'top': '0',
      'left': '0',
      'z-index': '-1',
      'width': '100%',
      'height': '100%',
      'background': 'rgba(0,0,0,.75)',
      'opacity': '0',
      'transition': '.25s opacity ease-in-out'
     });
 
     // Styling close button
     modalCloseBtn.css({
      'position': 'absolute',
      'top': '-25px',
      'right': '-26px',
      'padding-top': '.75rem',
      'padding-bottom': '.75rem',
      'width': '3rem',
      'font-size': '1rem',
      'color': '#fff',
      'border': '0',
      'border-radius': '50%',
      'opacity': '0',
      'cursor': 'pointer',
      'background': '#111',
      'transition': '.25s opacity ease-in-out'
     });
 
     // Styling close button - hover over overlay
     modalOverlay.on('mouseenter', function() {
      modalCloseBtn.css({'opacity': '.5'});
     });
 
     // Styling close button - mouse leaves the overlay
     modalOverlay.on('mouseleave', function() {
      modalCloseBtn.css({'opacity': '0'});
     });
 
     // Styling close button - hover over button itself
     modalOverlay.on('mouseenter', function() {
      modalCloseBtn.on('mouseenter', function() {
       $(this).css({'opacity': '1'});
      });
     });
 
     // Styling close button - mouse leaves the button
     modalOverlay.on('mouseleave', function() {
      modalCloseBtn.on('mouseleave', function() {
       $(this).css({'opacity': '.5'});
      });
     });
 
     // Styling the arrows
     $('.js-modal-next, .js-modal-prev').css({
      'position': 'absolute',
      'top': '50%',
      'z-index': '91',
      'display': 'block',
      'width': '3rem',
      'height': '3rem',
      'font-size': '32px',
      'font-weight': '400',
      'text-decoration': 'none',
      'color': '#fff',
      'background': '#111',
      'box-shadow': '0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.24)',
      'opacity': '0',
      'transition': '.25s all ease-in-out'
     });
     $('.js-modal-prev').css({
      'left': '0'
     });
     $('.js-modal-next').css({
      'right': '0'
     });
 
     // Styling arrows - hover over overlay
     modalOverlay.on('mouseenter', function() {
      $('.js-modal-next, .js-modal-prev').css({'opacity': '.5'});
     });
 
     // Styling arrows - mouse leaves the overlay
     modalOverlay.on('mouseleave', function() {
      $('.js-modal-next, .js-modal-prev').css({'opacity': '0'});
     });
 
     // Styling arrows - hover over the buttons
     $('.js-modal-next, .js-modal-prev').on('mouseenter', function() {
      $(this).css({
       'color': '#fff',
       'box-shadow': '0 3px 6px rgba(0,0,0,.16), 0 3px 6px rgba(0,0,0,.23)',
       'opacity': '1'
      });
     });
 
     // Styling arrows - mouse leaves the buttons
     $('.js-modal-next, .js-modal-prev').on('mouseleave', function() {
      $(this).css({
       'color': '#fff',
       'box-shadow': '0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.24)',
       'opacity': '.5'
      });
     });

     // Grid-Based Gallery
     $('.' + app.settings.elementClass).on('click', function() {
      // Itterating over all images in the gallery
      $('.' + app.settings.elementClass).each(function(index) {
       // Assigning data-index attribute with unique number
       // to every image in the gallery
       $(this).attr('data-index', index);
       $(this).css({'max-width': '100%'});
      });
      var currentImage = $(this), // Currently clicked image
          imgSrc = currentImage.attr('src'), // Src attribute of clicked image
          imgAlt = currentImage.attr('alt'), // Alt attribute of clicked image
          currentImageIndex = currentImage.data('index'), // Number from data-index attribute of clicked image
          imgArray = $('img[data-index]'), // array of all images
          numOfImages = imgArray.length - 1, // switch to 0-based index
          nextIndex, // prepare nextIndex variable
          prevIndex; // prepare prevIndex variable

      if (currentImageIndex > 0) {
      // If you are not on the first image, decrease the index by 1
       prevIndex = currentImageIndex - 1;
      } else {
       // If you are on the first image, go on the last one
       prevIndex = numOfImages;
      }
      if ((currentImageIndex + 1) < numOfImages) {
       // If you are not on the last image, increase the index by 1
       nextIndex = currentImageIndex + 1;
      } else {
       // If you are on the last image, go on the first one
       nextIndex = 0;
      }

      // Attach image to modal dialog
      function imageAttach() {
       modalImage.attr({
        src: imgSrc,
        alt: imgAlt,
        "data-index": currentImageIndex
       });
      }

     // Open modal dialog
     function modalOpen() {
      modalOverlay.css({
       'z-index': '90',
       'opacity': '1'
      });

      // Show previous image on "left arrow" key press
      $(document).on('keydown', function(e) {
       if (e.keyCode == 37 || e.charCode == 37 || e.which == 37) {
        prevImage(e);
       }
      });

      // Show next image on "right arrow" key press
      $(document).on('keydown', function(e) {
       if (e.keyCode == 39 || e.charCode == 39 || e.which == 39) {
        nextImage(e);
       }
      });

      // Close modal on "Esc" key press
      $(document).on('keydown', function(e) {
       if (e.keyCode == 27 || e.charCode == 27 || e.which == 27) {
        closeModal(e);
       }
      });
     }

     // Show previous image
     function prevImage(e) {
      e.preventDefault();
      modalImage.attr({
       src: $('[data-index=' + prevIndex + ']').attr('src'),
       alt: $('[data-index=' + prevIndex + ']').attr('alt'),
       'data-index': $('[data-index=' + prevIndex + ']').attr('data-index')
      });
      currentImageIndex = prevIndex;

      if (currentImageIndex > 0) {
       // If you are not on the first image, decrease the index by 1
       prevIndex = currentImageIndex - 1;
      } else {
       // If you are on the first image, go on the last one
       prevIndex = numOfImages;
      }
      if ((currentImageIndex + 1) < numOfImages) {
       // If you are not on the last image, increase the index by 1
       nextIndex = currentImageIndex + 1;
      } else {
       // If you are on the last image, go on the first one
       nextIndex = 0;
      }
     }

     // Show next image
     function nextImage(e) {
      e.preventDefault();
      modalImage.attr({
        src: $('[data-index=' + nextIndex + ']').attr('src'),
        alt: $('[data-index=' + nextIndex + ']').attr('alt'),
        'data-index': $('[data-index=' + nextIndex + ']').attr('data-index')
      });
      currentImageIndex = nextIndex;

      if (currentImageIndex > 0) {
       // If you are not on the first image, decrease the index by 1
       prevIndex = currentImageIndex - 1;
      } else {
       // If you are on the first image, go on the last one
       prevIndex = numOfImages;
      }
      if ((currentImageIndex + 1) <= numOfImages) {
       // If you are not on the last image, increase the index by 1
       nextIndex = currentImageIndex + 1;
      } else {
      // If you are on the last image, go on the first one
       nextIndex = 0;
      }
     }

     // Close modal dialog
     function closeModal(e) {
      e.preventDefault();
      modalOverlay.css({
       'z-index': '-1',
       'opacity': '0'
      });
     }

     // Attach clicked image to modal dialog
     imageAttach();

     // Open modal dialog
     modalOpen();

     // Handle click on right arrow
     nextImageAnchor.on('click', function(e) {
      nextImage(e);
     });

     // Handle click on left arrow
     prevImageAnchor.on('click', function(e) {
      prevImage(e);
     });

     // Handle click on close button
     modalCloseBtn.on('click', function(e) {
      closeModal(e);
     });
    });
   }
  },

  // Run the controllers
  init: function() {
   app.controllers();
  }
 };

 // Initialize the app
 app.init();
})($);

Closing thoughts

Congratulations! You faced this challenge and made it. Now, you have nice grid-based gallery with fully working custom preview in modal dialog. What’s more, this grid is, I think, easy to implement thanks to “outsourcing” the majority of work and tasks to JavaScript. All you need to is to use “gallery__grid” id and then specify what class is used to mark the images in gallery. Feel free to use this tutorial in your own projects. Also, don’t settle. Try to make it better. After that, you can sit, relax and enjoy the result of your work because you deserve it.

Before leaving you, let me give you a glimpse into the future. In the upcoming post some of the things you will have a chance to learn will be, for example, how to design an HTML email and also HTML banner ads. There will also be tutorials on getting started with grunt, yeoman, node.js, coffeescript, mongoDB, AngularJS, ruby, ruby on rails and much much more. As you can see, you have a lot to be excited about. I hope that you these future tutorials and guides will help you expand your skillset and get more and better jobs.

Thank you very much for your time. And, until next time, have a great day!

Did you like this article? Please subscribe.

Are you on social media? Let's connect! You can find me on Twitter and Dribbble.

Comments are closed.