Modals are widely used on the web, but they are rarely built with accessibility in mind. When a modal is displayed, the background is dimmed visually but it's still visible to screen readers and keyboard-only users.
In this post, the first of a two-part series, we'll look at presenting accessible modals in Rails using two different approaches powered by Hotwire's Turbo and Stimulus libraries.
But first, let's see what we need to do to make modals accessible.
How Can We Make Modals Accessible?
To make modals accessible, we need to:
- Hide the background from screen readers.
- Implement "focus trapping" so keyboard users cannot focus on elements outside the modal.
- Shift focus to the first focusable element in the modal (if there is one).
- Dismiss the modal with the
Esc
key.
We could accomplish the first two by setting inert="true"
on the background elements, but this attribute can't be set on an ancestor of the modal. We can do the last two with some more custom JavaScript. As you can imagine this is rather tedious.
The HTML <dialog>
element gives us most of the above list for free, and is supported for 94.25% of global users, meaning it's ideal in most cases.
Let's look a bit more closely at the <dialog>
element.
The <dialog>
Element
According to MDN:
The HTML
<dialog>
element is used to create both modal and non-modal dialog boxes. Modal dialog boxes interrupt interaction with the rest of the page being inert, while non-modal dialog boxes allow interaction with the rest of the page.
This means that a <dialog>
isn't necessarily a modal. It can be presented within the document flow as well.
Presenting a <dialog>
To present a dialog as a modal, we need to use JavaScript:
When presenting a dialog this way, the background is made inert, focus is trapped in the modal, and it can be dismissed using the Esc
key.
To present a <dialog>
in a non-modal context, and hence make it visible by default, we use the open
attribute:
We won't get the accessibility features using this method, though, as the dialog hasn't been presented "modally".
Dismissing a <dialog>
A modally presented <dialog>
can be dismissed using JavaScript:
It can also be closed using a <form>
with the dialog
method. This is a great way to implement a "close" button.
That covers the basics of the <dialog>
element, so let's look at using it with Hotwire.
Modal Presentation Using Stimulus in Ruby on Rails
Stimulus is a JavaScript library under the Hotwire umbrella. It allows us to attach pieces of JavaScript logic to HTML elements encapsulated in controllers. This post assumes a basic familiarity with its API.
Let's start with a Rails controller and view. As an example use case, we'll use a support page which has a button to display some contact details in a modal. Generate a controller and action using:
Amend the created route to a more user-friendly path:
Sketch out a button and dialog in the newly generated view file:
Run the Rails server and navigate to the /support
path. You should see a button there, but it doesn't do anything at the moment. Let's create a Stimulus controller and wire it up.
The Stimulus Controller in Rails
Use the generator to create a Stimulus controller.
When the button is clicked, we need to get a reference to the <dialog>
and call showModal()
on it. To keep the controller generic, we'll pass in the element's id
as a param.
We can now decorate the button with the required data-
attributes:
Refresh the page and the button should now work!
There's an opportunity to remove some boilerplate code here. The modal
controller should always be attached to a button that shows the modal. We can remove the data-action
from the markup and set it in the controller:
Styling the Modal
The dialog looks fairly basic, so let's add some styles:
The ::backdrop
pseudo-element is a really great way to style the modal's background!
Try using the Tab
key to navigate around the page and you'll see that the focus is trapped within the modal. It's worth reviewing the page with a screen reader as well.
This is the simplest way to modally present a <dialog>
. Next, we'll look at a server-driven way to present modals: Turbo Frame.
Turbo Frame Powered Modals for Ruby
Turbo Frames are a subset of the Turbo library which is also under the Hotwire umbrella. It allows us to scope navigation to specific parts of a page, updating them in isolation from the rest of the page.
We can use Turbo Frames to present a modal rendered from the server.
Setting Up
Let's add another button to display a modal contact form. We'll need a controller and action to render the form:
Replace the auto-generated routes with:
We'll also need a global Turbo Frame to render modals, so let's put it in the main application layout:
Add a link to show the contact form:
And you're done!
Rendering and Presenting the Form
Clicking the Show contact form link will expect a <turbo-frame>
with id="remote_modal"
in the response and update the global Turbo Frame with its content. Fill in the view with the form:
We've now got a text area in the modal which is a focusable element. For accessibility, it should be focused by default when the modal is presented. The autofocus
attribute is used to accomplish this.
Refresh the page and try clicking Show contact form. Nothing will happen visually, but on inspecting the HTML, you'll notice the <dialog>
has been rendered in the remote_modal
Turbo Frame. We haven't presented it yet, which is why it's invisible.
We could render it with the open
attribute, but that would defeat the purpose as it'd be presented in a non-modal context without the modal accessibility features.
Let's create another Stimulus controller to present the form:
And hook it up to the dialog:
Refresh the page and try viewing the contact form again. This time it should work. We've just rendered a modal from the server!
While this is quite convenient, it's not ideal to create a new Stimulus controller just to present a modal. There's another method we can use as well: Turbo Streaming Modals. We'll take a look at those in part two of this series.
Up Next: Turbo Streaming Modals
In this post, we explored two different methods to present modals using Hotwire: Stimulus and Turbo Frames.
In part two, we'll look at another way to present modals: using Turbo Streams.
Until then, happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!