& f e i h c s i Modal m g o l a di s a m m dile Hidde de Vries 8 June 2023 CSS Day, Amsterdam
A presentation at CSS Day in June 2023 in Amsterdam, Netherlands by Hidde de Vries
& f e i h c s i Modal m g o l a di s a m m dile Hidde de Vries 8 June 2023 CSS Day, Amsterdam
@hdv@front-end.social In the beginning, content was linear…
@hdv@front-end.social Today, it can overlap in all sorts of ways
@hdv@front-end.social Today, it can overlap in all sorts of ways
@hdv@front-end.social Today, it can overlap in all sorts of ways
The Economist: a popover to teach how the UI works @hdv@front-end.social
g o l a i <d HTML element ∙ wide browser support ∙ (as of recently) good accessibility support popover New attribute / API in HTML ∙ v1 supported in Chrome stable ∙ coming to other browsers too
g o l a i <d @hdv@front-end.social UI considerations HTML element with wide browser support and (as of recently) good accessibility support Semantics popover Positioning New attribute / API in HTML, v1 supported in Chrome stable, coming to other browsers too
@hdv@front-end.social Hi, I’m Hidde developer relations + accessibility at NL Design System participant at Open UI CG hidde.blog LIKE & SUBSCRIBE
@hdv@front-end.social
@hdv@front-end.social music films concerts books …
@hdv@front-end.social hidde.blog words
@hdv@front-end.social I read this book I went to a concert log.hidde.blog data points, things I liked I listened to a song fi I watched a lm
@hdv@front-end.social • • • Eleventy site Sanity data covers in many shapes and sizes
“palette”: { “muted”: { “background”: “#53aa51”, “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#fff”, “title”: “#fff”, “population”: 2.87 }, “lightVibrant”: { “title”: “#000”, “population”: 0, “background”: “#acec84”, “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#000” }, “darkVibrant”: { “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#fff”, “title”: “#fff”, “population”: 3.68, “background”: “#1a418d” }, “lightMuted”: { “title”: “#fff”, “population”: 1.93, “background”: “#9ac2a5”, “_type”: “sanity.imagePaletteSwatch”, @hdv@front-end.social
“palette”: { “muted”: { “background”: “#53aa51”, “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#fff”, “title”: “#fff”, “population”: 2.87 }, “lightVibrant”: { “title”: “#000”, “population”: 0, “background”: “#acec84”, “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#000” }, “darkVibrant”: { “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#fff”, “title”: “#fff”, “population”: 3.68, “background”: “#1a418d” }, “lightMuted”: { “title”: “#fff”, “population”: 1.93, “background”: “#9ac2a5”, “_type”: “sanity.imagePaletteSwatch”, @hdv@front-end.social
“palette”: { “muted”: { “background”: “#53aa51”, “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#fff”, “title”: “#fff”, “population”: 2.87 }, “lightVibrant”: { “title”: “#000”, “population”: 0, “background”: “#acec84”, “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#000” }, “darkVibrant”: { “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#fff”, “title”: “#fff”, “population”: 3.68, “background”: “#1a418d” }, “lightMuted”: { “title”: “#fff”, “population”: 1.93, “background”: “#9ac2a5”, “_type”: “sanity.imagePaletteSwatch”, @hdv@front-end.social
“palette”: { “muted”: { “background”: “#53aa51”, “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#fff”, “title”: “#fff”, “population”: 2.87 }, “lightVibrant”: { “title”: “#000”, “population”: 0, “background”: “#acec84”, “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#000” }, “darkVibrant”: { “_type”: “sanity.imagePaletteSwatch”, “foreground”: “#fff”, “title”: “#fff”, “population”: 3.68, “background”: “#1a418d” }, “lightMuted”: { “title”: “#fff”, “population”: 1.93, “background”: “#9ac2a5”, “_type”: “sanity.imagePaletteSwatch”, @hdv@front-end.social
@hdv@front-end.social books.njk
@hdv@front-end.social books.njk
@hdv@front-end.social books.njk
.book-case > span { @hdv@front-end.social aspect-ratio: 1 / 8; max-width: 2em; transform: rotate(-2deg); } books.css
.book-case > span { @hdv@front-end.social aspect-ratio: 1 / 8; max-width: 2em; transform: rotate(-2deg); } books.css
@hdv@front-end.social
@hdv@front-end.social dialogs vs popovers
Teams: a timed popover to ask for feedback @hdv@front-end.social
@hdv@front-end.social Teams: a popover to teach about an Excel integration
@hdv@front-end.social Teams: a popover to urge me to be my expressive self
@hdv@front-end.social Slack Huddles: a popover to tell the user they look nice today
Online banking: a popover to autocomplete search query @hdv@front-end.social
Online banking: a popover to autocomplete search query Online banking: a popover to select a transfer date @hdv@front-end.social
@hdv@front-end.social dialog What is it developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
@hdv@front-end.social dialog What is it Additional window to your main window. a.k.a. “descendant window” or “subwindow”
@hdv@front-end.social dialog What is it Usually contains an action or task for the user, sometimes has critical information
@hdv@front-end.social dialog What is it ‘a “conversation” between the system and the user’ https://carbondesignsystem.com/patterns/dialog-pattern/
fi fi dialog What is it (typically) Do you want to continue, yes or no If you want to open a new le, what shall we do with your current le? How do you want to crop this image, where is the hot spot?
@hdv@front-end.social
<dialog> What is it HTML element to build dialogs, with built-in dialog role and modal setting <dialog>… </dialog> html.spec.whatwg.org/dev/interactive-elements.html#the-dialog-element@hdv@front-end.social // show as modal element.showModal();
<dialog> Open/close with script // show as non-modal element.show();@hdv@front-end.social // dialog element <dialog>…</dialog> comes with role, modal setting, closeon-Esc etc
@hdv@front-end.social // dialog element <dialog>…</dialog> just the semantics, no behaviour comes with semantics, top layer, inertness, close-on-Esc etc // an element with // dialog semantics <div role=”dialog”>… </div>
@hdv@front-end.social // dialog element <dialog>…</dialog> just the semantics, no behaviour comes with semantics, top layer, inertness, close-on-Esc etc // an element with // dialog semantics <div role=”dialog”>… </div> “dialog” just the word
@hdv@front-end.social current status: <dialog>
@hdv@front-end.social popover What is it a loating piece of UI with supplemental or contextual content “non-modal dialog” “transient content” f “supplemental” from Dell DS, “contextual” + “non-modal dialog” from Lightning DS, “transient content” from Spectrum
@hdv@front-end.social [popover] What is it a set of behaviors that can be added to any element through the popover attribute <div popover>… </div> html.spec.whatwg.org/dev/popover.html#the-popover-attribute
@hdv@front-end.social [popover] Use cases <select>’s listbox content pickers form element suggestions action menus teaching UI
@hdv@front-end.social [popover] No JavaScript required <button> Toggle popover </button> <div> … </div>
@hdv@front-end.social [popover] No JavaScript required <button> Toggle popover </button> <div popover> … </div> make it a popover
@hdv@front-end.social [popover] No JavaScript required <button> Toggle popover </button> <div popover id=”p”> … </div> add a unique ID
@hdv@front-end.social point button to ID [popover] No JavaScript required <button popovertarget=”p”> Toggle popover </button> <div popover id=”p”> … </div>
@hdv@front-end.social do we want this for dialog too? github.com/whatwg/html/issues/3567
@hdv@front-end.social [popover] Can open, close or toggle <button popovertarget=”p” popovertargetaction=”show”> Open popover </button> <div popover id=”p”> … </div>
@hdv@front-end.social [popover] Has auto and manual modes // Closes other popovers // when opened; has // light dismiss. <div popover=”auto”> … </div>
@hdv@front-end.social [popover] Has auto and manual modes // Closes other popovers // when opened; has // light dismiss. <div popover=”auto”> … </div> // No closing of others, // no light dismiss <div popover=”manual”> … </div>
@hdv@front-end.social // show as popover element.showPopover(); [popover] Open/close with script
@hdv@front-end.social current status: popover attribute/api in Chromium 114 (Chrome stable, Edge stable soon) Tech Preview >167, Safari 17 (fall 2023)
@hdv@front-end.social 🤔 How are these patterns different?
@hdv@front-end.social modal vs non-modal
The Economist: a modal overlay for privacy consent @hdv@front-end.social
Dutch government: a modal to extend the DigiD session @hdv@front-end.social
A “game over” screen @hdv@front-end.social
The Economist: popover with options @hdv@front-end.social
Social network: a non-modal alternative text dialog @hdv@front-end.social
CMS: a non-modal menu for image options @hdv@front-end.social
Booking website: a non-modal chat widget @hdv@front-end.social
@hdv@front-end.social A modal element is a drastic measure, as the user can do nothing else. Use it sparingly!
@hdv@front-end.social modal vs non-modal light dismiss vs explicit dismiss
New message: explicit dismiss @hdv@front-end.social
Font chooser: light dismiss @hdv@front-end.social
@hdv@front-end.social light vs explicit dismiss Does the element automatically hide on click outside, scroll, etc? Or does user or script close it?
@hdv@front-end.social modal vs non-modal light dismiss vs explicit dismiss z-index vs top layer
@hdv@front-end.social fi fi With z-index, you can stack elements on top of each other. The element that is rst in the DOM is painted rst, each subsequent element on top of the previous and
@hdv@front-end.social fi fi With z-index, you can stack elements on top of each other. The element that is rst in the DOM is painted rst, each subsequent element on top of the previous and z-index: 1;
@hdv@front-end.social With z-index, you can stack elements on top of each other. The element that is rst in the DOM is painted rst, The element that each subsequent rstofin the DOM element onistop is painted rst, the previous and each subsequent element on top of the previous and fi fi fi fi z-index: 2;
@hdv@front-end.social The top layer is above everything else, its own layer above the main document ft fi fi fi fi https://dra s.csswg.org/css-position-4/#top-layer The element that is rst in the DOM is painted rst, The element that each subsequent rstofin the DOM element onistop is painted rst, the previous and each subsequent element on top of the previous and
@hdv@front-end.social The top layer is above everything else, its own layer above the main document ft https://dra s.csswg.org/css-position-4/#top-layer
@hdv@front-end.social Layered based on order of top layer addition, not z-index ft fi fi fi fi https://dra s.csswg.org/css-position-4/#top-layer The element that is rst in the DOM is painted rst, The element that each subsequent rstofin the DOM element onistop is painted rst, the previous and each subsequent element on top of the previous and top layer, top layer, top layer, top layer, top layer, top layer
@hdv@front-end.social Layered based on order of top layer addition, not z-index ft fi fi fi fi https://dra s.csswg.org/css-position-4/#top-layer The element that is rst in the DOM is painted rst, The element that each subsequent top layer, top rstofin the DOM element onistop layer, top layer, is painted rst, the previous and top layer, top each subsequent layer, top layer also top layer, element on top of also top layer, the previous and also top layer, also top layer,
@hdv@front-end.social modal vs non-modal light dismiss vs explicit dismiss z-index vs top layer backdrop
@hdv@front-end.social Sometimes elements have a backdrop. Top layer elements have a built-in styleable backdrop (::backdrop) https://fullscreen.spec.whatwg.org/#::backdrop-pseudo-element
@hdv@front-end.social modal vs non-modal light dismiss vs explicit dismiss z-index vs top layer backdrop keyboard focus trap
@hdv@front-end.social keyboard focus trap Sometimes you want to prevent users from exiting a component with their Tab key. This is always temporary.
@hdv@front-end.social modal vs non-modal light dismiss vs explicit dismiss z-index vs top layer backdrop keyboard focus trap
@hdv@front-end.social modal vs non-modal <dialog>
<dialog> with showModal() dismiss light dismiss vs explicit with show() popover vs top layer z-index backdrop keyboard focus trap@hdv@front-end.social modal vs non-modal light dismiss vs explicit dismiss popover=”auto” popover=”manual” z-index vs top layer backdrop any <dialog> keyboard focus trap
@hdv@front-end.social modal vs <dialog> popover non-modal <dialog> with showModal() vs explicit dismiss light dismiss with show() z-index vs top layer backdrop keyboard focus trap
@hdv@front-end.social modal vs non-modal w/ ::backdrop browser provided light dismiss vs consider if modal <dialog> not better popover explicit dismiss <dialog> with showModal() z-index vs top layer backdrop keyboard focus trap
@hdv@front-end.social modal vs non-modal w/ ::backdrop browser provided light dismiss vs consider if modal <dialog> not better popover explicit dismiss <dialog> with showModal() z-index vs top layer backdrop keyboard focus trap
@hdv@front-end.social modal vs non-modal light dismiss vs explicit dismiss browser provided vs top layer z-index <dialog> with showModal() backdrop keyboard focus trap
@hdv@front-end.social semantics
@hdv@front-end.social “What is this thing?”
@hdv@front-end.social “What is this thing on the page?”
@hdv@front-end.social
<h1> heading@hdv@front-end.social
<h1> heading@hdv@front-end.social <a> link
@hdv@front-end.social
<li> list item@hdv@front-end.social
<dialog> dialog@hdv@front-end.social
<div> [no role]@hdv@front-end.social
<div role=”link”> link@hdv@front-end.social
<div popover> [no role]@hdv@front-end.social The popover attribute adds behaviour, not semantics (you choose a role based on the situation)
@hdv@front-end.social For components that are like a smaller window / subwindow on top of the main page dialogs <dialog> or role=”dialog”
@hdv@front-end.social For components that are like a smaller window / subwindow on top of the main page
@hdv@front-end.social For components that are like a smaller window / subwindow on top of the main page
@hdv@front-end.social For components that let the user choose from a list, the listbox wraps the choices listbox role=”listbox”
@hdv@front-end.social For components that let the user choose from a list, the listbox wraps the choices listbox role=”listbox” listbox role=”listbox”
@hdv@front-end.social listbox role=”listbox”
@hdv@front-end.social For components that o fer the user a list of choices that are actions (like in an application). menus f role=”menu”
@hdv@front-end.social For components that o fer the user a list of choices that are actions (like in an application). menus f role=”menu”
@hdv@front-end.social For components that o fer the user a list of choices that are actions (like in an application). menus f role=”menu”
f marcozehe.de/wai-aria-menus-use-with-care/ not for navigations / meganavigations not to be confused with <menu>, which has a built-in list role could complicate things for screenreader users, use sparsely
@hdv@front-end.social Plain text suggestions tooltips role=”tooltip”
tooltips role=”dialog” More than plain text, maybe better as toggletips
@hdv@front-end.social dialogs <dialog> or role=”dialog” tooltips role=”tooltip” menus role=”menu” role=”dialog”
@hdv@front-end.social semantics <dialog> dialog (implicit) popover it depends. You choose an apt role, could be dialog, listbox, menu or tooltip
@hdv@front-end.social positioning
@hdv@front-end.social
g o l a i d < popover ) ( l a d o M w o h s h t i w Both are centered by default
@hdv@front-end.social
@hdv@front-end.social // concert-list.njk <ol> {% for concert in concerts %} <li> … </li> {% endfor %} </ol>
@hdv@front-end.social // concert-list.njk <ol> {% for concert in concerts %} <li> … {% if concert.images %} <button type=”button” data-dialogtarget=”{{ dialogID }}” aria-label=”Details for {{ concert.info }}” <svg aria-hidden=”true” focusable=”false”>…></svg> </button> {% endif %} </li> {% endfor %} </ol>
@hdv@front-end.social // concert-list.njk <ol> {% for concert in concerts %} <li> … {% if concert.images %} <button type=”button” data-dialogtarget=”{{ dialogID }}” aria-label=”Details for {{ concert.info }}” <svg aria-hidden=”true” focusable=”false”>…></svg> </button> <dialog id=”{{ dialogID }}” aria-label=”Details for {{ concert.info }}”> <button type=”button” data-dialogclose=”{{ dialogID }}” aria-label=”Close”> <svg aria-hidden=”true” focusable=”false”>… </button> … </dialog> {% endif %} </li> {% endfor %}
@hdv@front-end.social // concert-list.js const dialogOpeners = document.querySelectorAll(‘[data-dialogtarget]’);
@hdv@front-end.social // concert-list.js const dialogOpeners = document.querySelectorAll(‘[data-dialogtarget]’); for (let i = 0; i < dialogOpeners.length; i++) { const opener = dialogOpeners[i]; const correspondingDialog = document.querySelector( #${opener.getAttribute('data-dialogtarget')}
); }
@hdv@front-end.social // concert-list.js const dialogOpeners = document.querySelectorAll(‘[data-dialogtarget]’); for (let i = 0; i < dialogOpeners.length; i++) { const opener = dialogOpeners[i]; const correspondingDialog = document.querySelector( #${opener.getAttribute('data-dialogtarget')}
); opener.addEventListener(‘click’, function() { correspondingDialog.showModal(); }); }
@hdv@front-end.social
@hdv@front-end.social
@hdv@front-end.social
@hdv@front-end.social
@hdv@front-end.social
@hdv@front-end.social
g o l a i d < ✅ popover ) ( l a d o M w o h s h t i w Both are centered by default
@hdv@front-end.social // book-list.njk … <button type=”button” popovertarget=”filters”> Toggle filters </button> …
@hdv@front-end.social // book-list.njk … <button type=”button” popovertarget=”filters”> Toggle filters </button> <div popover role=”menu” id=”filters”> <button type=”button”> Show only Dutch authors </button> <button type=”button”> Show only books with blue covers </button> <button type=”button”> Show only books rated > 3 stars </button> </div> …
@hdv@front-end.social // book-list.njk … <button type=”button” popovertarget=”filters”> Toggle filters </button> <div popover role=”menu” id=”filters”> <button type=”button”> Show only Dutch authors </button> <button type=”button”> Show only books with blue covers </button> <button type=”button”> Show only books rated > 3 stars </button> </div> …
@hdv@front-end.social // book-list.njk … <button type=”button” popovertarget=”filters”> Show filters </button> <div popover role=”menu” id=”filters”> <button type=”button” role=”menuitem”> Show only Dutch authors </button> <button type=”button” role=”menuitem”> Show only books with blue covers </button> <button type=”button” role=”menuitem”> Show only books rated > 3 stars </button> </div> …
@hdv@front-end.social // book-list.njk … <button type=”button” popovertarget=”filters”> Show filters </button> <div popover role=”menu” id=”filters”> <button type=”button” role=”menuitem” autofocus> Show only Dutch authors </button> <button type=”button” role=”menuitem” tabindex=”-1”> Show only books with blue covers </button> <button type=”button” role=”menuitem” tabindex=”-1”> Show only books rated > 3 stars </button> </div> …
@hdv@front-end.social // book-list.njk … <button type=”button” popovertarget=”filters”> Show filters </button> <div popover role=”menu” id=”filters”> <button type=”button”> Show only Dutch authors </button> <button type=”button”> Show only books with blue covers </button> <button type=”button”> Show only books rated > 3 stars </button> </div> …
@hdv@front-end.social
g o l a i d < ✅ popover ) ( l a d o M w o h s h t i w ✅ Both are centered by default
@hdv@front-end.social popover positioning Option 1: calculate (yourself or with a library) // book-list.njk … <script type=”module”> import { computePosition } from ‘https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.9/+esm’; </script> …
@hdv@front-end.social popover positioning Option 1: calculate (yourself or with a library) // book-list.njk … <script type=”module”> import { computePosition } from ‘https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.9/+esm’; const popover = document.querySelector(‘[popover]’); </script> …
@hdv@front-end.social popover positioning Option 1: calculate (yourself or with a library) // book-list.njk … <script type=”module”> import { computePosition } from ‘https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.9/+esm’; const popover = document.querySelector(‘[popover]’); popover.addEventListener(‘toggle’, function(e) { }); </script> …
@hdv@front-end.social
popover positioning Option 1: calculate (yourself or with a library) // book-list.njk … <script type=”module”> import { computePosition } from ‘https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.9/+esm’; const popover = document.querySelector(‘[popover]’); popover.addEventListener(‘toggle’, function(e) { const invoker = document.querySelector([popovertarget="${popover.getAttribute('id')}"
); }); </script> …
@hdv@front-end.social
popover positioning Option 1: calculate (yourself or with a library) // book-list.njk … <script type=”module”> import { computePosition } from ‘https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.9/+esm’; const popover = document.querySelector(‘[popover]’); popover.addEventListener(‘toggle’, function(e) { const invoker = document.querySelector([popovertarget="${popover.getAttribute('id')}"
); if (e.newState === ‘open’) { } }); </script> …
@hdv@front-end.social
popover positioning Option 1: calculate (yourself or with a library) // book-list.njk … <script type=”module”> import { computePosition } from ‘https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.9/+esm’; const popover = document.querySelector(‘[popover]’); popover.addEventListener(‘toggle’, function(e) { const invoker = document.querySelector([popovertarget="${popover.getAttribute('id')}"
); if (e.newState === ‘open’) { computePosition(invoker, popover).then(({x, y}) => { Object.assign(popover.style, { left: ${x}px
, top: ${y}px
, }); }); } }); </script>
@hdv@front-end.social
popover positioning Option 1: calculate (yourself or with a library) // book-list.njk … <script type=”module”> import { computePosition } from ‘https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.9/+esm’; const popover = document.querySelector(‘[popover]’); popover.addEventListener(‘toggle’, function(e) { const invoker = document.querySelector([popovertarget="${popover.getAttribute('id')}"
); if (e.newState === ‘open’) { computePosition(invoker, popover).then(({x, y}) => { Object.assign(popover.style, { left: ${x}px
, top: ${y}px
, }); }); } }); </script>
@hdv@front-end.social popover positioning Option 1: calculate (yourself or with a library)
@hdv@front-end.social popover positioning Option 1: calculate (yourself or with a library) I did have to override the UA default for the popover’s margin and position
@hdv@front-end.social popover positioning Option 2: anchor positioning drafts.csswg.org/css-anchor-position-1 kizu.dev/anchor-positioning-experiments/
@hdv@front-end.social positioning Centered non-modal <dialog> In page low. Anchor with script now, later with anchor positioning popover Centered. Anchor with script now, later with anchor positioning f modal <dialog>
@hdv@front-end.social
<dialog> on popover What if my session timed out?@hdv@front-end.social
@hdv@front-end.social adrianroselli.com/2023/05/brief-note-on-popovers-with-dialogs.html Brief Note on Popovers with Dialogs Suggests only using popover if also using modal <dialog>, so that both are in top layer
@hdv@front-end.social wrapping up
g o l a i <d @hdv@front-end.social UI considerations HTML element with wide browser support and (as of recently) good accessibility support Semantics popover Positioning New attribute / API in HTML, v1 supported in Chrome stable, coming to other browsers too
thank you! Questions @hdv on most platforms Slides/resources talks.hiddedevries.nl