diff --git a/src/img/2026/snacking-seagull.jpg b/src/img/2026/snacking-seagull.jpg new file mode 100755 index 00000000..96c88c0a Binary files /dev/null and b/src/img/2026/snacking-seagull.jpg differ diff --git a/src/posts/2026/2026-03-22-accessible-image-modals.md b/src/posts/2026/2026-03-22-accessible-image-modals.md new file mode 100644 index 00000000..df02d4c2 --- /dev/null +++ b/src/posts/2026/2026-03-22-accessible-image-modals.md @@ -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 ``, a search for image modals will not get you there quickly. + +If you search for *accessible* image modals, you'll still hear about `
`s. Hell, [W3C's ARIA Authoring Practices Guide uses a `
`](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 `` element. Thanks Adrian! + +Did I roll my own modal at first? Regretfully, yes. I'd used the `` 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 ``. + +### what does `` give us? + +so, what do we get from the `` element that we don't get from a `
` 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 `` 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 `` element as it is not interactive and does not receive focus. + +— [``: 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 *`` 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 `` + +here's what else I wrote... + +### html + +besides the ``, I gave my `` 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 `` element and copies over the `src` and `alt` attributes - importantly, it doesn't copy the full `` because we *don't* want to copy that `tabindex` attribute +- replaces the current `` 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!