accessible image modals crosses fingers
This commit is contained in:
BIN
src/img/2026/snacking-seagull.jpg
Executable file
BIN
src/img/2026/snacking-seagull.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
92
src/posts/2026/2026-03-22-accessible-image-modals.md
Normal file
92
src/posts/2026/2026-03-22-accessible-image-modals.md
Normal file
@ -0,0 +1,92 @@
|
||||
---
|
||||
title: accessible image modals
|
||||
image:
|
||||
src: 2026/snacking-seagull.jpg
|
||||
alt: Image unrelated to post. A seagull floating in the water with a starfish hanging out of eir mouth.
|
||||
tags:
|
||||
- reference
|
||||
- software
|
||||
---
|
||||
|
||||
Recently I've been working on a single-page digital rendition of a zine complete with many hand-drawn images. The author wanted to be able to bring images up to a full-screen view, to either zoom in or to put the whole enlarged image on one screen with no scrolling. It was a real struggle to find resources on how to do this in an accessible manner, so I'm writing up what I did.
|
||||
|
||||
> Fair warning: This solution is likely imperfect.
|
||||
|
||||
## the `dialog` element
|
||||
|
||||
do you know how many tutorials want you to roll your own modals? It's a not-insignificant amount. W3Schools, top of the search results in many cases, recommends it in two places - [image modals](https://www.w3schools.com/howto/howto_css_modal_images.asp){target="_blank"} and [responsive image modals](https://www.w3schools.com/css/css3_images_modal.asp){target="_blank"}. Several search results for "image modal" pop up div solutions - [div modal 1](https://stackoverflow.com/questions/75598914/how-to-display-an-image-clicking-on-it-using-modal-window-on-html-css-and-js){target="_blank"}, [div modal 2](https://dev.to/salehmubashar/create-an-image-modal-with-javascript-2lf3){target="_blank"}, [div modal 3](https://www.youtube.com/watch?v=Y9TNHynFjaQ){target="_blank"}.
|
||||
|
||||
If you don't know of the existence of `<dialog>`, a search for image modals will not get you there quickly.
|
||||
|
||||
If you search for *accessible* image modals, you'll still hear about `<div>`s. Hell, [W3C's ARIA Authoring Practices Guide uses a `<div>`](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/){target="_blank"}. You kinda have to go digging to read about the [dialog element from Scott O'Hara](https://www.scottohara.me/blog/2023/01/26/use-the-dialog-element.html){target="_blank"} or find [AccessibleWeb.dev's piece on modals](https://accessibleweb.dev/modals){target="_blank"}. Or you can go [straight for Adrian Roselli](https://adrianroselli.com/2025/06/where-to-put-focus-when-opening-a-modal-dialog.html){target="_blank"} and find examples that use the native `<dialog>` element. Thanks Adrian!
|
||||
|
||||
Did I roll my own modal at first? Regretfully, yes. I'd used the `<dialog>` element before... several years ago... in a project I don't have access to anymore... Needless to say, I had forgotten about its existence.
|
||||
|
||||
Anyway, I got there. Eventually. So let's talk about modals and `<dialog>`.
|
||||
|
||||
### what does `<dialog>` give us?
|
||||
|
||||
so, what do we get from the `<dialog>` element that we don't get from a `<div>` modal?
|
||||
|
||||
1. a semantically meaningful element
|
||||
1. a backdrop that fills the screen behind the modal, styleable with `::backdrop`
|
||||
1. automatic use of the `Esc` key to close the modal
|
||||
1. automatic focus trapping that prevents any tabbing within the page behind the modal (users can tab off the page into the browser buttons)
|
||||
1. the JS functions `.showModal()` and `.close()`
|
||||
1. if `closedby` is set to `any`, clicking outside the modal (anywhere on the backdrop) will also close the modal
|
||||
1. automatic return of focus to the element that triggered the modal
|
||||
1. the `autofocus` attribute for in-modal elements to set which element should receive focus on opening the modal
|
||||
1. the `open` attribute which is set on the `<dialog>` when open and removed when closed (when you use the functions mentioned previously)
|
||||
|
||||
and more. This is just the pieces in use for me. There's also things like [setting forms so that submission closes the dialog](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#additional_notes){target="_blank"} (point 1 in that list).
|
||||
|
||||
### what doesn't it give us?
|
||||
|
||||
1. a close button - gotta roll your own and attach the requisite `.close()` call (or rely on users hitting escape or clicking the backdrop)
|
||||
1. prevention of scroll on the rest of the page
|
||||
|
||||
### a bug
|
||||
|
||||
MDN warns:
|
||||
|
||||
> Do not add the `tabindex` property to the `<dialog>` element as it is not interactive and does not receive focus.
|
||||
|
||||
— [`<dialog>`: additional notes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#additional_notes){target="_blank"} (point 3 in that list).
|
||||
|
||||
despite this, I found that in Firefox (but not Edge), the *`<dialog>` itself was focusable*. I have no idea why, but I tested this on a totally unstyled and unmodified page and still found it to be true. As a focusable element, it made no sense. It had no interactivity and could not be activated.
|
||||
|
||||
I'm still torn: do I add `tabindex="-1"`? MDN specifically says not to, but I'm pretty sure they're warning against making it *focusable*. Why warn against making it nonfocusable when *it's not supposed to be focusable in the first place*, after all?
|
||||
|
||||
At current, I'm ambivalent, but I've added `tabindex="-1"` to handle Firefox's poor behavior. Making the dialog focusable is unhelpful and confusing.
|
||||
|
||||
## in addition to the `<dialog>`
|
||||
|
||||
here's what else I wrote...
|
||||
|
||||
### html
|
||||
|
||||
besides the `<dialog>`, I gave my `<img>` elements `tabindex="0"` to make them focusable.
|
||||
|
||||
### js
|
||||
|
||||
in `modal.js`, I created an `openDialog()` function that takes in the clicked image. It:
|
||||
|
||||
- creates a new `<img>` element and copies over the `src` and `alt` attributes - importantly, it doesn't copy the full `<img>` because we *don't* want to copy that `tabindex` attribute
|
||||
- replaces the current `<img>` in the modal with our new copy with `replaceChild()` (or if there's no current one, it just appends)
|
||||
- calls `dialog.showModal()`
|
||||
|
||||
I gave my close button two event listeners, one that listens for click events and one that listens for a keydown of the space or enter keys. In the case of the keydown, it calls `event.preventDefault()` to stop the space key from scrolling the underlying page.
|
||||
|
||||
I also looped through all images and attached my `openDialog()` function to any image with a `tabindex` attribute (I had some images that weren't intended to be fullscreened, so they lacked `tabindex`). Again, I gave them listeners on both click and keydown.
|
||||
|
||||
### css
|
||||
|
||||
here's the most relevant parts of the CSS:
|
||||
|
||||
- `dialog::backdrop` was given a [relatively calculated](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Colors/Using_relative_colors){target="_blank"} color - `rgba(from var(--color-bg) r g b / .8)` - as well as a blur
|
||||
- `body:has(dialog[open])` has `overflow: hidden` set
|
||||
- `dialog img` uses a `max-height` as well as `object-fit: contain`
|
||||
|
||||
## errors? questions?
|
||||
|
||||
reach out!
|
||||
Reference in New Issue
Block a user