Table of Contents
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>⨯</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">‹</a><a href="#" class="js-modal-next modal__next theme-background">›</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.
If you liked this article, please subscribe so you don't miss any future post.
If you'd like to support me and this blog, you can become a patron, or you can buy me a coffee 🙂