Tag: Dialogs

Replace JavaScript Dialogs With the New HTML Dialog Element

You know how there are JavaScript dialogs for alerting, confirming, and prompting user actions? Say you want to replace JavaScript dialogs with the new HTML dialog element.

Let me explain.

I recently worked on a project with a lot of API calls and user feedback gathered with JavaScript dialogs. While I was waiting for another developer to code the <Modal /> component, I used alert(), confirm() and prompt() in my code. For instance:

const deleteLocation = confirm('Delete location'); if (deleteLocation) {   alert('Location deleted'); }

Then it hit me: you get a lot of modal-related features for free with alert(), confirm(), and prompt() that often go overlooked:

  • It’s a true modal. As in, it will always be on top of the stack — even on top of that <div> with z-index: 99999;.
  • It’s accessible with the keyboard. Press Enter to accept and Escape to cancel.
  • It’s screen reader-friendly. It moves focus and allows the modal content to be read aloud.
  • It traps focus. Pressing Tab will not reach any focusable elements on the main page, but in Firefox and Safari it does indeed move focus to the browser UI. What’s weird though is that you can’t move focus to the “accept” or “cancel” buttons in any browser using the Tab key.
  • It supports user preferences. We get automatic light and dark mode support right out of the box.
  • It pauses code-execution., Plus, it waits for user input.

These three JavaScripts methods work 99% of the time when I need any of these functionalities. So why don’t I — or really any other web developer — use them? Probably because they look like system errors that cannot be styled. Another big consideration: there has been movement toward their deprecation. First removal from cross-domain iframes and, word is, from the web platform entirely, although it also sounds like plans for that are on hold.

With that big consideration in mind, what are alert(), confirm() and prompt() alternatives do we have to replace them? You may have already heard about the <dialog> HTML element and that’s what I want to look at in this article, using it alongside a JavaScript class.

It’s impossible to completely replace Javascript dialogs with identical functionality, but if we use the showModal() method of <dialog> combined with a Promise that can either resolve (accept) or reject (cancel) — then we have something almost as good. Heck, while we’re at it, let’s add sound to the HTML dialog element — just like real system dialogs!

If you’d like to see the demo right away, it’s here.

A dialog class

First, we need a basic JavaScript Class with a settings object that will be merged with the default settings. These settings will be used for all dialogs, unless you overwrite them when invoking them (but more on that later).

export default class Dialog { constructor(settings = {}) {   this.settings = Object.assign(     {       /* DEFAULT SETTINGS - see description below */     },     settings   )   this.init() }

The settings are:

  • accept: This is the “Accept” button’s label.
  • bodyClass: This is a CSS class that is added to <body> element when the dialog is open and <dialog> is unsupported by the browser.
  • cancel: This is the “Cancel” button’s label.
  • dialogClass: This is a custom CSS class added to the <dialog> element.
  • message: This is the content inside the <dialog>.
  • soundAccept: This is the URL to the sound file we’ll play when the user hits the “Accept” button.
  • soundOpen: This is the URL to the sound file we’ll play when the user opens the dialog.
  • template: This is an optional, little HTML template that’s injected into the <dialog>.

The initial template to replace JavaScript dialogs

In the init method, we’ll add a helper function for detecting support for the HTML dialog element in browsers, and set up the basic HTML:

init() {   // Testing for <dialog> support   this.dialogSupported = typeof HTMLDialogElement === 'function'   this.dialog = document.createElement('dialog')   this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog'   this.dialog.role = 'dialog'      // HTML template   this.dialog.innerHTML = `   <form method="dialog" data-ref="form">     <fieldset data-ref="fieldset" role="document">       <legend data-ref="message" id="$ {(Math.round(Date.now())).toString(36)}">       </legend>       <div data-ref="template"></div>     </fieldset>     <menu>       <button data-ref="cancel" value="cancel"></button>       <button data-ref="accept" value="default"></button>     </menu>     <audio data-ref="soundAccept"></audio>     <audio data-ref="soundOpen"></audio>   </form>`    document.body.appendChild(this.dialog)    // ... }

Checking for support

The road for browsers to support <dialog> has been long. Safari picked it up pretty recently. Firefox even more recently, though not the <form method="dialog"> part. So, we need to add type="button" to the “Accept” and “Cancel” buttons we’re mimicking. Otherwise, they’ll POST the form and cause a page refresh and we want to avoid that.

<button$ {this.dialogSupported ? '' : ` type="button"`}...></button>

DOM node references

Did you notice all the data-ref-attributes? We’ll use these for getting references to the DOM nodes:

this.elements = {} this.dialog.querySelectorAll('[data-ref]').forEach(el => this.elements[el.dataset.ref] = el)

So far, this.elements.accept is a reference to the “Accept” button, and this.elements.cancel refers to the “Cancel” button.

Button attributes

For screen readers, we need an aria-labelledby attribute pointing to the ID of the tag that describes the dialog — that’s the <legend> tag and it will contain the message.

this.dialog.setAttribute('aria-labelledby', this.elements.message.id)

That id? It’s a unique reference to this part of the <legend> element:

this.dialog.setAttribute('aria-labelledby', this.elements.message.id)

The “Cancel” button

Good news! The HTML dialog element has a built-in cancel() method making it easier to replace JavaScript dialogs calling the confirm() method. Let’s emit that event when we click the “Cancel” button:

this.elements.cancel.addEventListener('click', () => {    this.dialog.dispatchEvent(new Event('cancel'))  })

That’s the framework for our <dialog> to replace alert(), confirm(), and prompt().

Polyfilling unsupported browsers

We need to hide the HTML dialog element for browsers that do not support it. To do that, we’ll wrap the logic for showing and hiding the dialog in a new method, toggle():

toggle(open = false) {   if (this.dialogSupported && open) this.dialog.showModal()   if (!this.dialogSupported) {     document.body.classList.toggle(this.settings.bodyClass, open)     this.dialog.hidden = !open     /* If a `target` exists, set focus on it when closing */     if (this.elements.target && !open) {       this.elements.target.focus()     }   } } /* Then call it at the end of `init`: */ this.toggle()

Keyboard navigation

Next up, let’s implement a way to trap focus so that the user can tab between the buttons in the dialog without inadvertently exiting the dialog. There are many ways to do this. I like the CSS way, but unfortunately, it’s unreliable. Instead, let’s grab all focusable elements from the dialog as a NodeList and store it in this.focusable:

getFocusable() {   return [...this.dialog.querySelectorAll('button,[href],select,textarea,input:not([type=&quot;hidden&quot;]),[tabindex]:not([tabindex=&quot;-1&quot;])')] }

Next, we’ll add a keydown event listener, handling all our keyboard navigation logic:

this.dialog.addEventListener('keydown', e => {   if (e.key === 'Enter') {     if (!this.dialogSupported) e.preventDefault()     this.elements.accept.dispatchEvent(new Event('click'))   }   if (e.key === 'Escape') this.dialog.dispatchEvent(new Event('cancel'))   if (e.key === 'Tab') {     e.preventDefault()     const len =  this.focusable.length - 1;     let index = this.focusable.indexOf(e.target);     index = e.shiftKey ? index-1 : index+1;     if (index < 0) index = len;     if (index > len) index = 0;     this.focusable[index].focus();   } })

For Enter, we need to prevent the <form> from submitting in browsers where the <dialog> element is unsupported. Escape will emit a cancel event. Pressing the Tab key will find the current element in the node list of focusable elements, this.focusable, and set focus on the next item (or the previous one if you hold down the Shift key at the same time).

Displaying the <dialog>

Now let’s show the dialog! For this, we need a small method that merges an optional settings object with the default values. In this object — exactly like the default settings object — we can add or change the settings for a specific dialog.

open(settings = {}) {   const dialog = Object.assign({}, this.settings, settings)   this.dialog.className = dialog.dialogClass || ''    /* set innerText of the elements */   this.elements.accept.innerText = dialog.accept   this.elements.cancel.innerText = dialog.cancel   this.elements.cancel.hidden = dialog.cancel === ''   this.elements.message.innerText = dialog.message    /* If sounds exists, update `src` */   this.elements.soundAccept.src = dialog.soundAccept || ''   this.elements.soundOpen.src = dialog.soundOpen || ''    /* A target can be added (from the element invoking the dialog */   this.elements.target = dialog.target || ''    /* Optional HTML for custom dialogs */   this.elements.template.innerHTML = dialog.template || ''    /* Grab focusable elements */   this.focusable = this.getFocusable()   this.hasFormData = this.elements.fieldset.elements.length > 0   if (dialog.soundOpen) {     this.elements.soundOpen.play()   }   this.toggle(true)   if (this.hasFormData) {     /* If form elements exist, focus on that first */     this.focusable[0].focus()     this.focusable[0].select()   }   else {     this.elements.accept.focus()   } }

Phew! That was a lot of code. Now we can show the <dialog> element in all browsers. But we still need to mimic the functionality that waits for a user’s input after execution, like the native alert(), confirm(), and prompt() methods. For that, we need a Promise and a new method I’m calling waitForUser():

waitForUser() {   return new Promise(resolve => {     this.dialog.addEventListener('cancel', () => {        this.toggle()       resolve(false)     }, { once: true })     this.elements.accept.addEventListener('click', () => {       let value = this.hasFormData ?          this.collectFormData(new FormData(this.elements.form)) : true;       if (this.elements.soundAccept.src) this.elements.soundAccept.play()       this.toggle()       resolve(value)     }, { once: true })   }) }

This method returns a Promise. Within that, we add event listeners for “cancel” and “accept” that either resolve false (cancel), or true (accept). If formData exists (for custom dialogs or prompt), these will be collected with a helper method, then returned in an object:

collectFormData(formData) {   const object = {};   formData.forEach((value, key) => {     if (!Reflect.has(object, key)) {       object[key] = value       return     }     if (!Array.isArray(object[key])) {       object[key] = [object[key]]     }     object[key].push(value)   })   return object }

We can remove the event listeners immediately, using { once: true }.

To keep it simple, I don’t use reject() but rather simply resolve false.

Hiding the <dialog>

Earlier on, we added event listeners for the built-in cancel event. We call this event when the user clicks the “cancel” button or presses the Escape key. The cancel event removes the open attribute on the <dialog>, thus hiding it.

Where to :focus?

In our open() method, we focus on either the first focusable form field or the “Accept” button:

if (this.hasFormData) {   this.focusable[0].focus()   this.focusable[0].select() } else {   this.elements.accept.focus() }

But is this correct? In the W3’s “Modal Dialog” example, this is indeed the case. In Scott Ohara’s example though, the focus is on the dialog itself — which makes sense if the screen reader should read the text we defined in the aria-labelledby attribute earlier. I’m not sure which is correct or best, but if we want to use Scott’s method. we need to add a tabindex="-1" to the <dialog> in our init method:

this.dialog.tabIndex = -1

Then, in the open() method, we’ll replace the focus code with this:


We can check the activeElement (the element that has focus) at any given time in DevTools by clicking the “eye” icon and typing document.activeElement in the console. Try tabbing around to see it update:

Showing the eye icon in DevTools, highlighted in bright green.
Clicking the “eye” icon

Adding alert, confirm, and prompt

We’re finally ready to add alert(), confirm() and prompt() to our Dialog class. These will be small helper methods that replace JavaScript dialogs and the original syntax of those methods. All of them call the open()method we created earlier, but with a settings object that matches the way we trigger the original methods.

Let’s compare with the original syntax.

alert() is normally triggered like this:


In our Dialog, we’ll add an alert() method that’ll mimic this:

/* dialog.alert() */ alert(message, config = { target: event.target }) {   const settings = Object.assign({}, config, { cancel: '', message, template: '' })   this.open(settings)   return this.waitForUser() }

We set cancel and template to empty strings, so that — even if we had set default values earlier — these will not be hidden, and only message and accept are shown.

confirm() is normally triggered like this:


In our version, similar to alert(), we create a custom method that shows the message, cancel and accept items:

/* dialog.confirm() */ confirm(message, config = { target: event.target }) {   const settings = Object.assign({}, config, { message, template: '' })   this.open(settings)   return this.waitForUser() }

prompt() is normally triggered like this:

window.prompt(message, default);

Here, we need to add a template with an <input> that we’ll wrap in a <label>:

/* dialog.prompt() */ prompt(message, value, config = { target: event.target }) {   const template = `   <label aria-label="$ {message}">     <input name="prompt" value="$ {value}">   </label>`   const settings = Object.assign({}, config, { message, template })   this.open(settings)   return this.waitForUser() }

{ target: event.target } is a reference to the DOM element that calls the method. We’ll use that to refocus on that element when we close the <dialog>, returning the user to where they were before the dialog was fired.

We ought to test this

It’s time to test and make sure everything is working as expected. Let’s create a new HTML file, import the class, and create an instance:

<script type="module">   import Dialog from './dialog.js';   const dialog = new Dialog(); </script>

Try out the following use cases one at a time!

/* alert */ dialog.alert('Please refresh your browser') /* or */ dialog.alert('Please refresh your browser').then((res) => {  console.log(res) })  /* confirm */ dialog.confirm('Do you want to continue?').then((res) => { console.log(res) })  /* prompt */ dialog.prompt('The meaning of life?', 42).then((res) => { console.log(res) })

Then watch the console as you click “Accept” or “Cancel.” Try again while pressing the Escape or Enter keys instead.


We can also use the async/await way of doing this. We’re replacing JavaScript dialogs even more by mimicking the original syntax, but it requires the wrapping function to be async, while the code within requires the await keyword:

document.getElementById('promptButton').addEventListener('click', async (e) => {   const value = await dialog.prompt('The meaning of life?', 42);   console.log(value); });

Cross-browser styling

We now have a fully-functional cross-browser and screen reader-friendly HTML dialog element that replaces JavaScript dialogs! We’ve covered a lot. But the styling could use a lot of love. Let’s utilize the existing data-component and data-ref-attributes to add cross-browser styling — no need for additional classes or other attributes!

We’ll use the CSS :where pseudo-selector to keep our default styles free from specificity:

:where([data-component*="dialog"] *) {     box-sizing: border-box;   outline-color: var(--dlg-outline-c, hsl(218, 79.19%, 35%)) } :where([data-component*="dialog"]) {   --dlg-gap: 1em;   background: var(--dlg-bg, #fff);   border: var(--dlg-b, 0);   border-radius: var(--dlg-bdrs, 0.25em);   box-shadow: var(--dlg-bxsh, 0px 25px 50px -12px rgba(0, 0, 0, 0.25));   font-family:var(--dlg-ff, ui-sansserif, system-ui, sans-serif);   min-inline-size: var(--dlg-mis, auto);   padding: var(--dlg-p, var(--dlg-gap));   width: var(--dlg-w, fit-content); } :where([data-component="no-dialog"]:not([hidden])) {   display: block;   inset-block-start: var(--dlg-gap);   inset-inline-start: 50%;   position: fixed;   transform: translateX(-50%); } :where([data-component*="dialog"] menu) {   display: flex;   gap: calc(var(--dlg-gap) / 2);   justify-content: var(--dlg-menu-jc, flex-end);   margin: 0;   padding: 0; } :where([data-component*="dialog"] menu button) {   background-color: var(--dlg-button-bgc);   border: 0;   border-radius: var(--dlg-bdrs, 0.25em);   color: var(--dlg-button-c);   font-size: var(--dlg-button-fz, 0.8em);   padding: var(--dlg-button-p, 0.65em 1.5em); } :where([data-component*="dialog"] [data-ref="accept"]) {   --dlg-button-bgc: var(--dlg-accept-bgc, hsl(218, 79.19%, 46.08%));   --dlg-button-c: var(--dlg-accept-c, #fff); } :where([data-component*="dialog"] [data-ref="cancel"]) {   --dlg-button-bgc: var(--dlg-cancel-bgc, transparent);   --dlg-button-c: var(--dlg-cancel-c, inherit); } :where([data-component*="dialog"] [data-ref="fieldset"]) {   border: 0;   margin: unset;   padding: unset; } :where([data-component*="dialog"] [data-ref="message"]) {   font-size: var(--dlg-message-fz, 1.25em);   margin-block-end: var(--dlg-gap); } :where([data-component*="dialog"] [data-ref="template"]:not(:empty)) {   margin-block-end: var(--dlg-gap);   width: 100%; }

You can style these as you’d like, of course. Here’s what the above CSS will give you:

Showing how to replace JavaScript dialogs that use the alert method. The modal is white against a gray background. The content reads please refresh your browser and is followed by a blue button with a white label that says OK.

Showing how to replace JavaScript dialogs that use the confirm method. The modal is white against a gray background. The content reads please do you want to continue? and is followed by a black link that says cancel, and a blue button with a white label that says OK.

Showing how to replace JavaScript dialogs that use the prompt method. The modal is white against a gray background. The content reads the meaning of life, and is followed by a a text input filled with the number 42, which is followed by a black link that says cancel, and a blue button with a white label that says OK.

To overwrite these styles and use your own, add a class in dialogClass,

dialogClass: 'custom'

…then add the class in CSS, and update the CSS custom property values:

.custom {   --dlg-accept-bgc: hsl(159, 65%, 75%);   --dlg-accept-c: #000;   /* etc. */ }

A custom dialog example

What if the standard alert(), confirm() and prompt() methods we are mimicking won’t do the trick for your specific use case? We can actually do a bit more to make the <dialog> more flexible to cover more than the content, buttons, and functionality we’ve covered so far — and it’s not much more work.

Earlier, I teased the idea of adding a sound to the dialog. Let’s do that.

You can use the template property of the settings object to inject more HTML. Here’s a custom example, invoked from a <button> with id="btnCustom" that triggers a fun little sound from an MP3 file:

document.getElementById('btnCustom').addEventListener('click', (e) => {   dialog.open({     accept: 'Sign in',     dialogClass: 'custom',     message: 'Please enter your credentials',     soundAccept: 'https://assets.yourdomain.com/accept.mp3',     soundOpen: 'https://assets.yourdomain.com/open.mp3',     target: e.target,     template: `     <label>Username<input type="text" name="username" value="admin"></label>     <label>Password<input type="password" name="password" value="password"></label>`   })   dialog.waitForUser().then((res) => {  console.log(res) }) });

Live demo

Here’s a Pen with everything we built! Open the console, click the buttons, and play around with the dialogs, clicking the buttons and using the keyboard to accept and cancel.

So, what do you think? Is this a good way to replace JavaScript dialogs with the newer HTML dialog element? Or have you tried doing it another way? Let me know in the comments!

Replace JavaScript Dialogs With the New HTML Dialog Element originally published on CSS-Tricks. You should get the newsletter and become a supporter.


, , , , ,

Choice Words about the Upcoming Deprecation of JavaScript Dialogs

It might be the very first thing a lot of people learn in JavaScript:

alert("Hello, World");

One day at CodePen, we woke up to a ton of customer support tickets about their Pens being broken, which ultimately boiled down to a version of Chrome that shipped where they ripped out alert() from functioning in cross-origin iframes. And all other native “JavaScript Dialogs” like confirm(), prompt() and I-don’t-know-what-else (onbeforeunload?, .htpasswd protected assets?).

Cross-origin iframes are essentially the heart of how CodePen works. You write code, and we execute it for you in an iframe that doesn’t share the same domain as CodePen itself, as the very first line of security defense. We didn’t hear any heads up or anything, but I’m sure the plans were on display.

I tweeted out of dismay. I get that there are potential security concerns here. JavaScript dialogs look the same whether they are triggered by an iframe or not, so apparently it’s confusing-at-best when they’re triggered by an iframe, particularly a cross-origin iframe where the parent page likely has little control. Well, outside of, ya know, a website like CodePen. Chrome cite performance concerns as well, as the nature of these JavaScript dialogs is that they block the main thread when open, which essentially halts everything.

There are all sorts of security and UX-annoyance issues that can come from iframes though. That’s why sandboxing is a thing. I can do this:

<iframe sandbox></iframe>

And that sucker is locked down. If some form tried to submit something in there: nope, won’t work. What if it tries to trigger a download? Nope. Ask for device access? No way. It can’t even load any JavaScript at all. That is, unless I let it:

<iframe sandbox="allow-scripts allow-downloads ...etc"></iframe>

So why not an attribute for JavaScript dialogs? Ironically, there already is one: “allow-modals“. I’m not entirely sure why that isn’t good enough, but as I understand it, nuking JavaScript dialogs in cross-origin iframes is just a stepping stone on the ultimate goal: removing them from the web platform entirely.

Daaaaaang. Entirely? That’s the word. Imagine the number of programming tutorials that will just be outright broken.

For now, even the cross-origin removal is delayed until January 2022, but as far as we know this is going to proceed, and then subsequent steps will happen to remove them entirely. This is spearheaded by Chrome, but the status reports that both Firefox and Safari are on board with the change. Plus, this is a specced change, so I guess we can waggle our fingers literally everywhere here, if you, like me, feel like this wasn’t particularly well-handled.

What we’ve been told so far, the solution is to use postMessage if you really absolutely need to keep this functionality for cross-origin iframes. That sends the string the user uses in window.alert up to the parent page and triggers the alert from there. I’m not the biggest fan here, because:

  1. postMessage is not blocking like JavaScript dialogs are. This changes application flow.
  2. I have to inject code into users code for this. This is new technical debt and it can harm the expectations of expected user output (e.g. an extra <script> in their HTML has weird implications, like changing what :nth-child and friends select).
  3. I’m generally concerned about passing anything user-generated to a parent to execute. I’m sure there are theoretical ways to do it safely, but XSS attack vectors are always surprising in their ingenouity.

Even lower-key suggestions, like window.alert = console.log, have essentially the same issues.

Allow me to hand the mic over to others for their opinions.

Couldn’t the alert be contained to the iframe instead of showing up in the parent window?

Jaden Baptista, Twitter

Yes, please! Doesn’t that solve a big part of this? While making the UX of these dialogs more useful? Put the dang dialogs inside the <iframe>.

“Don’t break the web.” to “Don’t break 90% of the web.” and now “Don’t break the web whose content we agree with.”

Matthew Phillips, Twitter

I respect the desire to get rid of inelegant parts [of the HTML spec] that can be seen as historical mistakes and that cause implementation complexity, but I can’t shake the feeling that the existing use cases are treated with very little respect or curiosity.

Dan Abramov, Twitter

It’s weird to me this is part of the HTML spec, not the JavaScript spec. Right?!

I always thought there was a sort of “prime directive” not to break the web? I’ve literally seen web-based games that used alert as a “pause”, leveraging the blocking nature as a feature. Like: <button onclick="alert('paused')">Pause</button>[.] Funny, but true.

Ben Lesh, Twitter

A metric was cited that only 0.006% of all page views contain a cross-origin iframe that uses these functions, yet:

Seems like a misleading metric for something like confirm(). E.g. if account deletion flow is using confirm() and breaks because of a change to it, this doesn’t mean account deletion flow wasn’t important. It just means people don’t hit it on every session.

Dan Abramov, Twitter

That’s what’s extra concerning to me: alert() is one thing, but confirm() literally returns true or false, meaning it is a logical control structure in a program. Removing that breaks websites, no question. Chris Ferdinandi showed me this little obscure website that uses it:

Speaking of Chris:

The condescending “did you actually read it, it’s so clear” refrain is patronizing AF. It’s the equivalent of “just” or “simply” in developer documentation.

I read it. I didn’t understand it. That’s why I asked someone whose literal job is communicating with developers about changes Chrome makes to the platform.

This is not isolated to one developer at Chrome. The entire message thread where this change was surfaced is filled with folks begging Chrome not to move forward with this proposal because it will break all-the-things.

Chris Ferdinandi, “Google vs. the web”

And here’s Jeremy:

[…] breaking changes don’t happen often on the web. They are—and should be—rare. If that were to change, the web would suffer massively in terms of predictability.

Secondly, the onus is not on web developers to keep track of older features in danger of being deprecated. That’s on the browser makers. I sincerely hope we’re not expected to consult a site called canistilluse.com.

Jeremy Keith, “Foundations”

I’ve painted a pretty bleak picture here. To be fair, there were some tweets with the Yes!! Finally!! vibe, but they didn’t feel like critical assessments to me as much as random Google cheerleading.

Believe it or not, I generally am a fan of Google and think they do a good job of pushing the web forward. I also think it’s appropriate to waggle fingers when I see problems and request they do better. “Better” here means way more developer and user outreach to spell out the situation, way more conversation about the potential implications and transition ideas, and way more openness to bending the course ahead.

The post Choice Words about the Upcoming Deprecation of JavaScript Dialogs appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , , , ,

Using for Menus and Dialogs is an Interesting Idea

One of the most empowering things you can learn as a new front-end developer who is starting to learn JavaScript is to change classes. If you can change classes, you can use your CSS skills to control a lot on a page. Toggle a class to one thing, style it this way, toggle to another class (or remove it) and style it another way.

But there is an HTML element that also does toggles! <details>! For example, it’s definitely the quickest way to build an accordion UI.

Extending that toggle-based thinking, what is a user menu if not for a single accordion? Same with modals. If we went that route, we could make JavaScript optional on those dynamic things. That’s exactly what GitHub did with their menu.

Inside the <details> element, GitHub uses some Web Components (that do require JavaScript) to do some bonus stuff, but they aren’t required for basic menu functionality. That means the menu is resilient and instantly interactive when the page is rendered.

Mu-An Chiou, a web systems engineer at GitHub who spearheaded this, has a presentation all about this!

The worst strike on <details> is its browser support in Edge, but I guess we won’t have to worry about that soon, as Edge will be using Chromium… soon? Does anyone know?

The post Using <details> for Menus and Dialogs is an Interesting Idea appeared first on CSS-Tricks.


, , , ,