Creating a Simple Lightbox Vue Component

Lightboxes are very popular, mainly if our application works a lot with images. Let’s see how to create a dead simple, yet a quite flexible lightbox Vue component.

Recently we faced a situation, that we had to integrate a lightbox functionality in an application. However, the nodes that held the images were injected after an AJAX call, so the classic way of integration did not work. We took a look at some solutions – popular and less popular as well – and we decided to create a very simple alternative.

The Parameters of the Lightbox

Well, as I said, we wanted to keep it really simple, so we summarized the parameters we have already or we need to implement:

  • in the project we used bulma, so we hade a very good base in term of HTML and CSS,
  • also, we used material design icons, so it was convenient to use them, however pulling it just for this would be huge overkill,
  • we decided not to have preloading or animations,
  • it should navigate when pressing arrows and quit when pressing escape,
  • it has to be dynamic – in the terms of reloading its items and the event bindings easily.

The Mechanics

So basically, how should this work? Well, we need to select nodes with the same parameter – probably a class name – and bind a click event to them. Mostly a node would look like this, but it really can be anything else as well:

<a href=large.png" class="lightbox-item">
    <img src="small.png">
</a>

Now, we can select all the nodes with the .lightbox-item class and add the click event listener to them. When it’s been clicked, we grab the href attribute’s value and load it into the lightbox. But, let’s see how does it work in code.

The Lightbox Component

Before we start: we decided to configure the container where the items are held, but in many cases probably it’s not needed, but you can just run the querySelectorAll on the document.

<script>
    export default {
        props: {
            container: {
                type: [String, Node]
            }
        },

        mounted() {
            this.init();

            window.addEventListener('keydown', event => {
                if (! this.isOpen) {
                    return
                } else if (event.keyCode === 27) {
                    this.close();
                } else if (event.keyCode === 37) {
                    this.prev();
                } else if (event.keyCode === 39) {
                    this.next();
                }
            });
        },

        data() {
            return {
                index: 0,
                items: [],
                isOpen: false
            };
        },

        computed: {
            current() {
                return this.items[this.index] || {};
            }
        },

        methods: {
            init(container = null) {
                if (! this.container && ! container) {
                    return;
                }

                container = container || this.container;
                container = typeof container === 'string' ? document.querySelector(container) : container;

                this.items = Array.from(container.querySelectorAll('.lightbox-item'));
                this.items.filter(item => !! item.href).forEach(item => {
                    item.addEventListener('click', event => {
                        event.preventDefault();
                        this.index = this.find(item);
                        this.open();
                    });
                });
            },
            open() {
                this.isOpen = true;
                this.$emit('open');
            },
            close() {
                this.isOpen = false;
                this.$emit('close');
            },
            next() {
                this.index = this.index === this.items.length - 1 ? 0 : this.index + 1;
            },
            prev() {
                this.index = this.index === 0 ? this.items.length - 1 : this.index - 1;
            },
            find(item) {
                return this.items.indexOf(item);
            }
        }
    }
</script>

<template>
    <div class="modal" :class="{ 'is-active': isOpen }">
        <div class="modal-background" @click="close"></div>
        <div class="modal-content is-clipped">
            <div class="columns is-gapless" style="height: 100%">
                <div v-if="items.length > 1" class="column is-1" style="display: flex; justify-content: center; align-items: center;">
                    <a class="icon is-large has-text-centered has-text-white" @click="prev">
                        <i class="mdi mdi-36px mdi-arrow-left"></i>
                    </a>
                </div>
                <div class="column has-text-centered">
                    <img :src="current.href" alt="" style="max-height: 100%; width: auto;">
                </div>
                <div v-if="items.length > 1" class="column is-1" style="display: flex; justify-content: center; align-items: center;">
                    <a class="icon is-large has-text-centered has-text-white" @click="next">
                        <i class="mdi mdi-36px mdi-arrow-right"></i>
                    </a>
                </div>
            </div>
        </div>
        <button class="modal-close is-large" aria-label="close" @click.prevent.stop="close"></button>
    </div>
</template>
Note, we can pass string and node value as a container as well. It means you can give a valid selector or a node instance too.

After registering the component, we can add the lightbox anywhere:

<lightbox container="#images"></lightbox>

Also, if you need to reload the lightbox because you load nodes dynamically, you can do the following:

<lightbox ref="lb" container="#images"></lightbox>

....

this.$refs.lb.init(this.$el);

Summary

Compared to the complex lightbox solutions, for sure, this knows very little. But this can be an excellent first step and if you need to add preloading, animation, error handling or anything else, you can easily do it by extending this code.

Also, it’s not a big deal to replace MDI and bulma and roll out a custom design implementation to make it more fluent with your app’s or site’s design.

Need a web developer? Maybe we can help, get in touch!