Codebase modals follow some aspects of the AlpineJS modal component design pattern. The accessibility features are the same but there are a few differences in the way modals are built here.
The simplest modal
Modal example 1a – without “click outside to dismiss”:
Modal content. Click the “Close” button to dismiss.
<div
x-data="{ open: false }"
x-id="['modal']"
>
<!-- Modal open button -->
<button
type="button"
class="btn-primary"
@click="open = true"
:aria-controls="$id('modal')"
:aria-expanded="open"
>Open modal</button>
<!-- Modal (cover/backdrop with panel inside) -->
<div
x-cloak
x-show="open"
x-trap.noscroll.inert="open"
x-transition.opacity.duration.600ms
@keydown.escape.prevent.stop="open = false"
role="dialog"
:id="$id('modal')"
aria-modal="true"
:aria-label="$id('modal')"
class="fixed box flex flex-center flex-middle bg-black-glass-3 z-index-999"
>
<!-- Modal panel -->
<div
class="w-xs m-3 b-thin rounded p-2 bg-white"
>
<div class="h3">Modal title</div>
<p>Modal content. Click the “Close” button to dismiss.</p>
<!-- Modal clode button -->
<button
type="button"
class="float-right"
@click="open = false">Close</button>
</div>
</div>
</div>
Notes on modals
- There are four basic elements to a Codebase modal component:
- The modal open button – control to open the modal
- The modal consisting of the full-viewport cover (a.k.a. backdrop), and the modal panel is inside it. The modal cover can be transparant (clear), or tinted (these Codebase examples use a semi-transparent black with blur-filter), or totally black if the modal is an image lightbox
- The modal panel containing your modal content
- The modal close button (a.k.a. dismiss button), usually placed within the modal panel but sometimes (e.g. for a lightbox) placed in a top corner of the cover
x-cloak
is used to hide the modal before AlpineJS has hidden it programatically. Codebase already contains the style[x-cloak] {display: none;}.
- The modal open and close
<button>
’s are not being used as part of a form. Therefore they must have the attributetype="button"
- The modal cover (wrapper) covers the entire screen once the modal is opened. It is required for centering and middling the modal panel in the viewport window using flexbox. The modal cover is transparent by default, but you can give it a dark blurry tint using
bg-black-glass-3
. Alternatively, you can usebg-black
if you are using the modal as a lightbox. - The modal panel will become as wide as its content requires. But you can constrain the width using one of Codebase’s (max-width) width constraint wrappers.
w-xs
is commonly used for narrow modals. - The modal can be opened open/close by the modal open button (mouse click, touch screen tap, etc.) and by tabbing onto the modal open button and hitting the enter key or the space bar. The modal open button operates as a toggle so that keyboard and assistive tech users can immediately close it again if they want to. However, mouse and touch screen users can’t toggle it closed because the modal will have opened and covered the viewport. Therefore a modal closed button needs to be supplied. (And an optional “click outside to dismiss” function.)
- Modals require the AlpineJS Focus plugin for keeping the focus on the modal once it has been tabbed into, because you don’t want to allow tabbers to move the focus out of the modal until it has been dismissed. ALpine’s
x-trap
puts the focus on the first focusable element inside the modal. Usually you will want this to be the modal close button. - Keyboard users can click the modal close button by tabbing onto it, then using the hitting the enter key or the space bar to dismiss the modal. The focus will then be returned to the dropdown button.
- The modal can be closed by hitting the escape key, whether the focus is on the modal open button or somewhere in the modal. And the focus will be returned to the modal open button.
- Modals require some accessibility features such as
role="dialog"
andaria-modal="true"
. The example Codebase/ AlplineJS powered modals on this page should have all the accessibility features that you need, following the MDN Accessibility (ARIA) guidelines forrole="dialog"
. (If something is wrong or missing here, please open a pull request and provide an explanation and example.) - All CSS transitions are handled by AlpineJS x-transition.
- If you want “click outside to dismiss”, you can easily add using AlpineJS
@click.outside="open = false"
to themodal-panel
. - The ID of the modal (used by
aria-controls=""
on its respective modal open button) has been assigned using the Alpine x-id directive, which automatically adds an incremented number to your specified ID (prefix). That way, you can include several modal components to a webpage without having to think up an ID for each one. (However, this won’t work if you have your modal open button in a separate Alpine component.)
Adding “click outside to dismiss”
Simply add @click.outside
to the modal panel.
Modal example 1b – with “click outside to dismiss”:
Modal content. Click the “Close” button to dismiss – or click outside to dismiss.
<!-- Modal panel -->
<div
class="w-xs m-3 b-thin rounded p-2 bg-white"
@click.outside="open = false"
>
...
</div>
Adding a panel reveal animation
You can add some animations to the modal panel using Codebase animations and Alpine 3’s x-transition directive.
Note: since these x-transition
animations are being applied to the modal panel, therefore you will also need to put x-show="open"
on the panel as well as on the modal cover.
Modal example 1c – with CSS transform scale 75%-100%:
Modal content. Click the “Close” button to dismiss – or click outside to dismiss.
<!-- Modal panel -->
<div
class="w-xs m-3 b-thin rounded p-2 bg-white"
@click.outside="open = false"
x-show="open"
x-transition:enter="transition-all-300ms"
x-transition:enter-start="scale-75%"
x-transition:enter-end="scale-100%"
x-transition:leave="transition-all-300ms"
x-transition:leave-start="scale-100%"
x-transition:leave-end="scale-75%"
>
...
</div>
Open button in a separate Alpine component
Until Alpine v.3, having a toggler or button as a separate component was possible using the Alpine $dispatch
magic property and the window as an event bus (see Hugo Di Francesco, https://codewithhugo.com/alpinejs-component-communication-event-bus/).
But Alpine 3 has built-in global state storage, using Alpine.store()
(see https://alpinejs.dev/globals/alpine-store). So, we can use that instead.
Modal example 1d – with a control button in a separate Alpine component:
Modal title
Modal content.
This is conceptually simpler than the window bus:
<!-- The expandedState store -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('modalEx1d', {
expandedState: false,
})
})
</script>
<!-- The (separate) button component -->
<div
x-data
@keydown.escape.prevent.stop="$store.modalEx1d.expandedState = false"
class="mb-2"
>
<button
type="button"
class="btn-primary"
@click="$store.modalEx1d.expandedState = true"
aria-controls="modal-ex-separate"
:aria-expanded="$store.modalEx1d.expandedState"
>Open modal</button>
</div>
<!-- Modal component without button
(cover/backdrop with panel inside) -->
<div x-data>
<div
x-cloak
x-show="$store.modalEx1d.expandedState"
x-transition.opacity.duration.600ms
x-trap.noscroll.inert="$store.modalEx1d.expandedState"
@keydown.escape.prevent.stop="$store.modalEx1d.expandedState = false"
role="dialog"
id="modal-ex-separate"
aria-modal="true"
:aria-label="modal-ex-separate"
class="fixed box flex flex-center flex-middle bg-black-glass-3 z-index-999"
>
<!-- Modal panel -->
<div
class="w-xs p-3 b-thin rounded bg-white"
@click.outside="$store.modalEx1d.expandedState = false"
>
<p id="modal-1d-title" class="h3">Modal title</p>
<p>Modal content.</p>
<!-- Modal close button -->
<button
type="button"
class="float-right"
@click="$store.modalEx1d.expandedState = false"
>Close</button>
</div>
</div>
</div>
Notes:
- There needs to be
@keydown.escape.prevent.stop=""
on both components – the separate button and the modal itself. - Since the open button and the modal are in two separate components, they can’t share an
x-id
. So you will need to set up the modalid
,aria-controls
andaria-label
another way (manually or programatically). - Having a button in a separate Alpine component enables you to have the button and the modal in different parts of a webage. And, if required in your design, it enables you to have more than one button per modal.
Styling modal panels
Here are a few examples of what you can do with Codebase CSS styling.
A fancy card panel
Above, you’ve seen simple modal panels that have a thin border, rounded corners, and a white background. Following is an alternative panel design example.
Modal example 2a – panel is dressed as a card, with the close button over the top right corner, and “click outside to dismiss”:
Modal header
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Panel with grid layout
Modal example 2b – panel with a grid layout, and “click outside to dismiss”:
Modal Header
Notes:
- The
h-max-100%
is necessary for small viewports (phones) where the panel body will be displayed as a single column.h-max-100%
works for the max available height, which also works well with iPhone Safari’s retractable browser bars. - The
flex flex-column
on the panel andgrow
on the panel body enables theoverflow-y
on the panel body to scroll vertically if required for very small viewports.
Panel with scrolling body
Modal panels that contain (or may contain) a lot of content can break on small screen devices (phones) where their content becomes too tall for the viewport.
Modal example 2c – tall panel without “click outside to dismiss”:
Header
Lorem ipsum dolor sit amet, vis in blandit singulis, an unum doming facilisi vim. Facete aliquam bonorum id quo, ex labore tincidunt mel, usu no quod liberavisse. Ex sea dolorum insolens assueverit, sed ut harum latine dignissim. Vis cibo vidit ea, eu duo debet platonem explicari, pro ex graece meliore. Illum graeci inciderint mei et, ei decore nostro vim.
Number of items:
Lorem ipsum dolor sit amet
|
Notes:
- The panel
h-max-100%
works for the max available height, which also works well with iPhone Safari’s retractable browser bars. - The
flex flex-column
on the panel andgrow
on the panel body enables theoverflow-y
on the panel body to scroll vertically.
Embedded popout modals
Most modals are invisible (and inaccessible) until triggered, but some are embedded in a page. Popout modals can be used for displaying a larger product image on an exommerce site (“zooming in”), detail-dense infographics, maps, etc.
Modal example 3 – modal panel as an embedded popout:
Codebase popout modal demo
Notes:
- You will need to add overflow scrolling on the popout panel if you have a lot of content, especially if it will be too much for smaller viewports.
- If you don’t want the popout to operate on smaller viewports, you can simply hide your “expand popout” button, e.g. using
md:hidden-below
.
Modal as a lightbox
Modal example 4a – lightbox with a tall image:
Modal example 4b – lightbox with a wide image:
Modal example 4c – lightbox with a tall and wide image:
Notes:
- Use
bg-black
on the modal (cover/backdrop) instead ofbg-black-glass-3
, so that the page content doesn’t distract from the image in the lightbox. - Place the modal close button in the modal (cover/backdrop) instead of in the modal panel.
- Place the image and title (caption) directly in the modal – you don’t need a modal panel element.
- You don’t need or a “click-away” function.
- The
img-contain
utility class on the<img>
makes it max-out at the height and/or width of the viewport (or rather, the modal), depending on how tall or wide it is. - You will need to optimize your image file-size, or use a lazy loading technique if your image is heavy and therefore slows down the page load time, especially on slow internet connections (think of phones).