.gallery
    .gallery__intro
        h1.gallery__title Lightbox, is that still a thing?
        p Well I needed a simple, fullscreen, css based, touch/keyboard friendly lightbox/slideshow and here it is! The script takes in a selector, queries it for all items and creates a lightbox/slideshow out of them.
        p this is the most basic version. The next version will be animated, probably with the popmotion library.
        p Any tips to make this script a bit more 'pluggable' are most welcome in the comments!
    ul
        li.gallery__item
            a.gallery__itemLink(href="https://farm5.staticflickr.com/4390/36837410552_88e7054780_k_d.jpg" data-rel='aiLightbox')
                img.gallery__itemThumb(src="https://farm5.staticflickr.com/4390/36837410552_fbeb6d1cdc_q_d.jpg")
        li.gallery__item
            a.gallery__itemLink(href="https://farm5.staticflickr.com/4357/36172707494_53c0f25d98_h_d.jpg" data-rel='aiLightbox')
                img.gallery__itemThumb(src="https://farm5.staticflickr.com/4357/36172707494_2b8ea05f04_q_d.jpg")
        li.gallery__item
            a.gallery__itemLink(href="https://farm5.staticflickr.com/4347/36857337771_4092a556e7_k_d.jpg" data-rel='aiLightbox')
                img.gallery__itemThumb(src="https://farm5.staticflickr.com/4347/36857337771_d1bb7f798a_q_d.jpg")
        li.gallery__item
            a.gallery__itemLink(href="https://farm5.staticflickr.com/4421/36997408855_a0a13bae79_k_d.jpg" data-rel='aiLightbox')
                img.gallery__itemThumb(src="https://farm5.staticflickr.com/4421/36997408855_bfdb9f1fba_q_d.jpg")
        li.gallery__item
            a.gallery__itemLink(href="https://farm5.staticflickr.com/4388/36996947395_8577ae759c_k_d.jpg" data-rel='aiLightbox')
                img.gallery__itemThumb(src="https://farm5.staticflickr.com/4388/36996947395_db3c348ccb_q_d.jpg")
        li.gallery__item
            a.gallery__itemLink(href="https://farm5.staticflickr.com/4379/36844732181_a6b854636e_k_d.jpg" data-rel='aiLightbox')
                img.gallery__itemThumb(src="https://farm5.staticflickr.com/4379/36844732181_78bd19e9eb_q_d.jpg")
        li.gallery__item
            a.gallery__itemLink(href="https://farm5.staticflickr.com/4333/36825444362_d157245677_k_d.jpg" data-rel='aiLightbox')
                img.gallery__itemThumb(src="https://farm5.staticflickr.com/4333/36825444362_4291ce6d1e_q_d.jpg")
        li.gallery__item
            a.gallery__itemLink(href="https://farm5.staticflickr.com/4348/36844765631_1f3df298d8_k_d.jpg" data-rel='aiLightbox')
                img.gallery__itemThumb(src="https://farm5.staticflickr.com/4348/36844765631_03e90607bd_q_d.jpg")
View Compiled
* {
    box-sizing: border-box;
}

@import 'breakpoint';

body {
    margin: 0;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: #ececec;
    font-family: 'Roboto Slab', 'Courier New', Courier, monospace;
}

h1,
h2,
h3,
h4, {
    font-family: 'Montserrat', sans-serif;
    font-weight: 800;
}

.gallery {
    
    & {
        max-width: calc((150px*2) + 1em);
        padding: 3em 0;
        color: #333;

        @include breakpoint(768px) {
            max-width: calc((150px*4) + 3em);   
        }
    }
    
    &__title {
        font-size: 2em;
        padding-bottom: 0.25em;
        border-bottom: 1px solid #666;
    }
    
    &__intro {
        margin: 0 0 2em;
        
        p {
            line-height: 1.5em;
        }
    }
    
    ul {
        list-style: none;
        margin: 0;
        padding: 0;
        width: 100%;
        display: flex;
        justify-content: space-between;
        flex-wrap: wrap;
    }
    
    li {
        margin-bottom: 1em;
        border-radius: 3px;
        box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
        transform: scale(1);
        transition: all 0.3s cubic-bezier(.25,.8,.25,1);
        
        &:hover {
            transform: scale(1.0221);
            box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
        }
    }
    
    img {
        display: block;
        border-radius: 3px;
    }
}

.m-lightbox {
    
    & {
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        background: rgba(#fff, 0.9);
        z-index: 1;
        opacity: 0;
        transform: scale(0.8);
        transition: opacity 0.3s ease-out, transform 0.3s ease-out;
        pointer-events: none;
    }
    
    &.is-active {
        opacity: 1;
        transform: scale(1);
        z-index: 101;
        pointer-events: auto;
    }
    
    &__slider {
        list-style: none;
        margin: 0;
        padding: 0;
        width: 100vw;
        height: 100vh;
    }
    
    &__slide {
        position: absolute;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        
        img {
            display: block;
            max-width: calc(100vw - 2em);
            max-height: 90vh;
            opacity: 0;
            transition: opacity 0.3s ease;
            
            @include breakpoint(768px) {
                max-width: calc(100vw - 116px);
                max-height: 90vh;
            }
        }
        
        &.is-loaded.is-active {
            img {
                opacity: 1;
            }
            
            .spinner {
                display: none;
            }
        }
    }
    
    button {
        position: absolute;
        margin: 0;
        padding: 0;
        z-index: 102;
        background: transparent;
        border: none;
        cursor: pointer;
    }
    
    &__close {        
        top: 1em;
        right: 1em;        
    }
    
    &__nextPrev {
        top: 50%;
        transform: translateY(-50%);
        width: 42px;
        height: 42px;
        visibility: hidden;
        opacity: 0;
        transform: scale(0.5);
        transition: 
            opacity 0.3s ease-out,
            transform 0.3s ease-out;
        
        &.is-active {
            visibility: hidden;
            transform: scale(1);
            opacity: 1;
            
            @include breakpoint(1024px) {
                visibility: visible;
            }
        }
        
        svg {
            display: block;
            width: 100%;
            height: auto;
        }
        
        &--next {
            right: 1em;
        }
        
        &--prev {
            left: 1em;
        }
        
        &:hover {
            cursor: pointer;
        }
    }
    
    &__counter {
        position: absolute;
        bottom: 1em;
        left: 50%;
        transform: translateX(-50%);
        color: #333;
        font-weight: 700;
    }
}


.spinner {
    
    & {
        width: 40px;
        height: 40px;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translateY(-50%) translateX(-50%);
    }
    
    &::before,
    &::after {
        content: '';
        width: 100%;
        height: 100%;
        border-radius: 50%;
        background-color: #333;
        opacity: 0.6;
        position: absolute;
        top: 0;
        left: 0;
        animation: sk-bounce 2.0s infinite ease-in-out;
    }
    
    &::after {
        animation-delay: -1.0s;    
    }
}

@keyframes sk-bounce {
    0%, 100% { 
        transform: scale(0.0);
    
    } 50% { 
        transform: scale(1.0);
    }
}

@import url('https://fonts.googleapis.com/css?family=Montserrat:800|Roboto+Slab:400,700');
View Compiled
class Lightbox {
    
    constructor({
        lazyload = true,
        counter = true,
        arrows = true,
        slideSpeed = 400,
        ...options
    }) {
        if(!options.selector) {
            console.error('Please add a valid css selector with the option "selector:"');
        } else if (typeof options.selector !== 'string') { 
            console.error(options.selector, `is not a string but a(n) ${typeof options.selector}`)   
        } else {
            this.selector = options.selector;
            this.lazyload = lazyload;
            this.counter = counter;
            this.arrows = arrows;
            this.slideSpeed = slideSpeed;
            
            this.links = Array.from(document.querySelectorAll(`a${options.selector}`));
            this.offsets = [];
            this.nodes = {};        
            this.imageIndex = null;
            if (this.links.length > 0) {
                this.createLightbox();
                this.createNodes();
                this.eventListeners(this.links);  
            } else {
                console.error(`The selector '${this.selector}' did not yield results. Please make sure the selector is applied on an 'a' element.`)
            }
        }
    };
    
    goTo = (num, event) => {
        const { items, counter, lightboxNode } = this.nodes;
        if(this.counter) {
            counter.innerHTML = `${num + 1}/${this.links.length}`;
        }
        const spinner = `<div class="spinner"></div>`;
        const img = items[num].querySelector('img');
        if(this.lazyload) {
            const src = img.getAttribute('data-src');
            items[num].insertAdjacentHTML('beforeend', spinner);
            // Set image attribute
            img.setAttribute('src', src);
            
            // Add class to slide item when image is completely loaded. Must be in this order. 
            const imgLoad = new Image();        
            imgLoad.onload = () => { 
                items[num].classList.add('is-active');
                items[num].classList.add('is-loaded');
            }
            imgLoad.src = src; 
        } else {
            items[num].classList.add('is-active');
            items[num].classList.add('is-loaded');
        }
        
        // Change the offset for each slide based on its index and the current index.
        for (let i = 0; i < this.offsets.length; i++) {
            const offset = this.offsets[i] - (num * 100);
            
            items[i].style.transform = `translateX(${offset}vw)`;
            
            // Add transition type based on which event was triggered
            if(event) {
                if(event.target.className === 'gallery__itemThumb') {
                    items[i].style.transition = `opacity 0.4s ease`;
                } else {
                    items[i].style.transition = `transform ${this.slideSpeed}ms ease-out`;
                }
            }
        }
    };

    createNodes = (links) => {
        // Find all the lightbox nodes and add them to an object
        Object.assign(this.nodes, {
            lightboxNode: document.querySelector('.m-lightbox'),
            items: Array.from(document.querySelectorAll('.m-lightbox__slide')),
            next: document.querySelector('.m-lightbox__nextPrev--next'),
            prev: document.querySelector('.m-lightbox__nextPrev--prev'),
            close: document.querySelector('.m-lightbox__close'),
            
        });
        
        Object.assign(this.nodes, { 
            counter: document.querySelector('.m-lightbox__counter'),
        });
    };

    eventListeners = (links) => {
        const { lightboxNode, items, next, prev, close } = this.nodes;
        links.forEach((item, index) => {
            item.addEventListener('click', (e) => {
                e.preventDefault();
                lightboxNode.classList.add('is-active');
                document.body.style.overflow = 'hidden';
                this.imageIndex = index;
                this.goTo(index, e);
                this.setNav(index);
            });
        });
        
        next.addEventListener('click', (e) => {
            this.goToNext(e);
        });
        
        prev.addEventListener('click', (e) => {
            this.goToPrev(e);
        });
        
        close.addEventListener('click', () => {
            this.closeBox();
        });
        
        document.onkeydown = (e) => {
            switch (e.keyCode) {
                case 37:
                    this.goToPrev(e);
                    break;
                case 39:
                    this.goToNext(e);
                    break;
                case 27:
                    this.closeBox();
                    break;
            };
        };
        
        items.forEach((item) => {
            // https://gist.github.com/Tam/d44c87b3daeb07b15984ddc6127d4e34
            new Swipe(item.querySelector('img'), (e, direction) => {
                e.preventDefault();
                switch (direction) {
                    case "up":
                        // Handle Swipe Up
                        break;
                    case "down":
                        // Handle Swipe Down
                        break;
                    case "left":
                        this.goToNext(e);
                        break;
                    case "right":
                        this.goToPrev(e);
                        break;
                }
            });
        })
    };

    setNav = (index) => {
        if(this.arrows) {
            const { next, prev } = this.nodes;
            if (index < (this.links.length - 1)) {                    
                next.classList.add('is-active');
            }        
            if (index >= (this.links.length - 1)) {                    
                next.classList.remove('is-active');
            } 
            if (index > 0) {
                prev.classList.add('is-active');
            }
            if(index <= 0) {
                prev.classList.remove('is-active');
            }
        }
    };

    goToNext = (e) => {
        const { items } = this.nodes;
        if (this.imageIndex < items.length - 1) {
            this.goTo((this.imageIndex + 1), e);
            setTimeout(() => {
                items[this.imageIndex - 1].classList.remove('is-active');
            }, this.slideSpeed)
            this.imageIndex += 1;
            this.setNav(this.imageIndex);
        }
    };

    goToPrev = (e) => {
        const { items } = this.nodes;
        if (this.imageIndex > 0) {
            this.goTo((this.imageIndex - 1), e);
            setTimeout(() => {
                items[this.imageIndex + 1].classList.remove('is-active');
            }, this.slideSpeed)
            this.imageIndex -= 1;
            this.setNav(this.imageIndex);
        }
    };

    closeBox = () => {
        const { lightboxNode, items } = this.nodes;
        lightboxNode.classList.remove('is-active');
        document.body.style.overflow = 'auto';
        setTimeout(() => {
            items.forEach(item => item.classList.remove('is-active'));
        }, this.slideSpeed);
    };
    
    renderImages = (images) => {
        const imagesLinks = images.map((item, index) => {
            const offset = index * 100;
            this.offsets.push(offset);
            const imageSrc = item.getAttribute('href');
            return `
                <li class='m-lightbox__slide' style='transform: translateX(${offset}vw)'>
                    ${this.lazyload ? `
                        <img data-src='${imageSrc}'/>
                    ` : `
                        <img src='${imageSrc}'/>
                    `}
                </li>
            `;
        });
        return imagesLinks;
    };
    
    createLightbox = () => {
        const lightbox = `
            <div class='m-lightbox'>
                <div class='m-lightbox__controls'>
                    <button class='m-lightbox__close'>
                        <svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
                            <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
                            <path d="M0 0h24v24H0z" fill="none"/>
                        </svg>
                    </button>
                    <button class='m-lightbox__nextPrev m-lightbox__nextPrev--prev'>
                        <svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
                            <path d="M0 0h24v24H0z" fill="none"/>
                            <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
                        </svg>
                    </button>
                    <button class='m-lightbox__nextPrev m-lightbox__nextPrev--next'>
                        <svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
                            <path d="M0 0h24v24H0z" fill="none"/>
                            <path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/>
                        </svg>
                    </button>
                </div>
                <ul class='m-lightBox__slider'>
                    ${this.renderImages(this.links).join('')}
                </ul>
                <div class='m-lightbox__counter'>
                </div>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', lightbox);
    };    
}

const lb = new Lightbox({
    selector: '[data-rel="aiLightbox"]', // string
    lazyload: true, // boolean
    arrows: true, // boolean    
    counter: true, // boolean
    slideSpeed: 500, //number(ms)
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.rawgit.com/Tam/d44c87b3daeb07b15984ddc6127d4e34/raw/3277237bf9f4f5893d505de796b33ae11271aaec/Swipe.js