Introduction #
In this article, I will describe modal component created using Alpine JS and Twig templating language. It is designed to be used with Craft CMS, but with little modifications, it will work in any system that uses Twig - for example, Drupal. It can also probably be easily ported to Blade, Smarty, or other templating systems.
If you want to quickly see what component looks like, you can visit demo page. You can grab component code from github gists.
Here is how the modal component looks like - it contains only bare-bones styling and is content-agnostic. In this case, we injected picture of kitten and header into it.
Summary of modal functionality. It does pretty much what most commonly used modals do:
- Modal is displayed and hidden using simple fade-in and fade-out animations. When it is opened, website content underneath it is covered with overlay and scrolling is blocked (thanks to setting
overflow: hidden
to<html>
tag). - Modal container has specific width on desktop (
40rem
, set using CSS custom property), while on mobile it will just fit the screen. Modal content can be scrolled if it does not fit the screen height. - Modal can be closed by clicking on x icon in the upper right corner or just by clicking outside of its container. Clicking inside, while letting on mouse button outside will not close modal.
- Modal contents are sitting within
<template>
element when modal is not displayed. Thanks to that, if modal has images inside, they will not get unnecessarily loaded before the user opens modal. - Modal contains ARIA attributes that make it accessible.
Usage #
Links to Alpine CDN are included in the component, so you don't need to attach any additional libraries to your website. Content is inserted into modal using {% embed %} Twig tag. You also need to provide a modal handle, so multiple instances of modal can be used. Handle cannot contain dashes.
{% embed 'path_to_modal_file' with {
modalHandle: 'someModal'
} %}
{% block modalContent %}
<div>our modal content</div>
{% endblock %}
{% endembed %}
Thanks to usage of embed
tag, modal functionality can fit seamlessly into your existing Twig templates. You don't need to put HTML content into Javasript variable or use DOM selectors, as is sometimes a case with modal libraries.
There are three Javascript functions that are available outside the modal component and are used to interact with it:
alpineModalOpen()
function is used to open modal. The first param of function is a modal handle, second one is optional array of options. This array can contain onOpen
option, that can be set to callback function that will run after modal opens - useful for dynamically replacing modal contents, for example with something returned by AJAX call.
alpineModalOpen('someModal');
// or:
alpineModalOpen('someModal', {
onOpen: function(){
document.querySelector('[data-modal-content]').innerHTML = 'something';
}
})
alpineModalClose()
is used to programatically close modal. It also requires modal handle parameter:
alpineModalClose('someModal')
alpineModalCloseAll()
can be used to close all modals, regardless of their handle.
alpineModalCloseAll()
If you want to close modal using some additional button placed inside its content, you can use Alpine @click
attribute and set it to closeModal()
. This is the internal method of Alpine component that closes modal - keep in mind that it will only work inside component:
{% embed 'path_to_modal_file' with {
modalHandle: 'someModal'
} %}
{% block modalContent %}
<div>our modal content</div>
<button @click="closeModal()">cancel</button>
{% endblock %}
{% endembed %} #}
Since modal contents are appended to DOM from inside <template>
tag when modal is not opened, you cannot attach JS events to them with regular way. You need to use event delegation - here's jQuery example:
// will work
$('body').on('click', '.some-class-inside-modal', function(){
// do dsomething..
})
// won't work
$('.some-class-inside-modal').on('click', function(){
// do dsomething..
})
Customizing modal #
Modal componenent was created with customization in mind. You can easily add new blocks to it, for example block that contains header - just add it to modal-component__content
element:
<div
class="modal-component__content"
data-modal-content
>
<div class="custom-modal-header">
{% block modalContent %}{% endblock %}
</div>
{% block modalContent %}{% endblock %}
</div>
Modal uses just Bare-bones styling. You can easily add your own decorations, like curved borders and box-shadow. You can also easily change color of background, width or padding, using custom properties that can be overwritten:
--modal-background-outside: rgba(0,0,0,0.8);
--modal-background-inside: white;
--modal-width: 40rem;
--modal-padding: 1rem;
If you have multiple modals, each of them must have a unique handle. You can give these modal differing styles - modal element will have CSS class based on its handle, like modal-component--someHandle
.
About Alpine JS #
Alpine JS is a library designed to add bits of interactivity to server-generated templates. Its syntax is based on Vue JS, but it does not offer any kind of component functionality - it just adds reactivity to markup using HTML attributes. That's why we need to rely on Twig templating language to create Alpine components and include them in our code. And that's why it is a perfect match for Craft CMS.
Teaching Alpine is beyond scope of this article - so I suggest anyone interested to head to one of these resources:
- https://github.com/alpinejs/alpine
- https://www.smashingmagazine.com/2020/03/introduction-alpinejs-javascript-framework/
- https://codewithhugo.com/tags/alpinejs/
But why build modal in Alpine JS at all, instead of using some modal library? Thanks to Alpine JS, we can easily customize how our modal behaves.
For example, there's one thing that really annoyed me about most modal libraries - how they handled closing modal when the user clicked outside of it. Or not really outside - when the user clicked inside and let of mouse button outside, it was still registered as outside click and triggered the closing of modal. Now imagine that modal contains contact form. Users, (me included) often click inside specific input to type there, and before they start typing, they let of the mouse button - often while the cursor already moved a bit. If cursor moved outside of modal border, modal closes. And all form contents are lost.
Building modal functionality using Alpine allowed me to easily remedy such problems and customize its functionality on a level usually not achievable with most modal libraries.