Ensuring your Users are on the Right Path: the Future of Modals and Focus Management


With *dialog* and the inert attribute landing in all major browsers in 2022, we as web developers now have simple yet powerful primitives to help build complex app-like flows on the web, rather than the over-engineered or leaky solutions we've relied on for years. Let's demystify these primitives and talk through how they make your code simpler: from plain HTML, Web Components, to React/similar.



♪♪ ♪♪ Hey there, my name's Sam, and I'm here today to talk about ensuring your users are on the right path, the futures of modals, and focus management. Now, that's a handful, and I submitted this talk to JS Nation a while ago, and since then, I think what it should be called is basically focus management in 2022. Now, the web has come a long way, and actually, for the first time this year, all the evergreens are supporting these amazing new features that we can use as either primitives or as part of some greater library. So, firstly, what is focus management, and why do you care? Now, I feel like this is the quintessential reason of why it's important. Now, we've got dialogue here, I can click around, I can hit submit, but what I can actually do while this is out operating is press tab a bunch of times and get behind this UI. I can still click some buttons that are on the page here. Now, you know, this is a demo, but lots of sites have this problem if you look around, and the challenge is that it's just really hard, and for the longest time, you know, we've got libraries and polyfills that try to really maintain this, you know, to try to solve this problem for you, but either they're not used correctly, they just can't do what you want them to do, or people just forget, and they just use a really basic mechanism and hope that users will never get there. And this example is obviously pretty tricky, you know, not everyone knows how to tab behind UI elements, but this kind of problem will leak out in other ways, causing users some weird behavior, and some examples here. To be very precise, the things I want to talk about today are dialogue and a note. Now, these are the two new concepts that are actually available in browsers this year, and they're both quite cool. They're actually quite old in terms of specs, they were written a long time ago, but like, as I've mentioned, it's the first time that you can use them in pretty much all evergreens with no hassle. So before we go any further, who is this guy? Why am I talking to you? I'm a former Googler, I was there for over 10 years, it was obviously really fun, I was on the developer relations team, and so part of my job was to do this kind of outreach. I've actually recently left Google and gone to a small early-stage startup in the green energy space called GridCog. It's a lot of fun, I would highly recommend working in climate, it's an important thing, and I'm always happy to talk to people about that orthogonally to the content of this talk. Having said that, our technology is still on the web, and so we want to use these features even in our SaaS product to make our user experiences better. And my general vibe of why I'm coming here and talking about these concepts in general, and why I want this in my browser, and why I want this in my web app, is that to me, these new features are letting us throw away tons of old code that did weird hacks and horrible things to browsers that really they weren't designed to do. So having new primitives in 2022 is really lovely, right? I can get rid of all this code and get on with the more interesting parts of my job, right? Not dealing with focus management, but actually writing applications that do interesting things. So the first component I'm going to talk about today is called the dialogue element. Now, dialogue can do a whole bunch of stuff. The spec is pretty wide. But the reason why we most care about it as web developers is I can create this element on my page wherever I like and then call show modal in JavaScript. What this does is then creates that dialogue in what's called a top layer. Now, the top layer is special. It does two things for us. One is that I can't tap behind it. You know, this focus problem has been solved. While it's visible, I can't interact with anything below it, including other dialogues that I previously opened. The second thing that's really cool here is that it actually exists outside the browser's kind of normal rendering layer. It's in a thing called the top layer. And what that means is even if you're inside some weird stacking context or your Z indexes are all bizarre, then actually the dialogue will always be on top. I actually like to demonstrate this by, if you can see here, that I've got this weird element that's transformed in bizarre ways. If I click this button, the dialogue is actually inside this transformed element, but the browser is just going to ignore all of that and hoist it to the top. So in that way, it's quite cool. You know, it's scoped and encapsulated in a way that you can use it in a component or some library and it will still appear right at the top, ready for the user to interact with it. Now, that's the most interesting part of the dialogue for me, but there's a few other things I want to cover before we go on. One of them is this backdrop element. And so every dialogue for free will have like a full screen element. You can change its color or background or whatever. It also has a built-in handler for the escape key. Now this is kind of bizarre, but kind of reflects when it was created back in 2014. But if I can hit escape on my keyboard now, then this dialogue will close. And that's fine, but a bit weird because there's no analogy on mobile or other platforms. You know, you can't press your back button on your mobile phone to close it. That will still, unfortunately, go to the previous page. There is something coming on the pipeline called Close Watcher as a bit of an aside, which is totally in draft state at this point, which does help deal with things like closing dialogues. And in fact, it's one of the examples called out in its description. But who knows if that'll come about. My advice, honestly, is if you don't want this behavior, you can actually add an event handler for the cancel event and then the user hitting escape will do nothing, right? And you can control that keyboard interaction or maybe you close the dialogue with buttons. You can do that entirely yourself. The next thing I want to talk about is Inert. Now this is a whole different feature. It has nothing to do with the previous dialogue, but it also lets you build interesting apps together. This is something where I can mark any subtree of my DOM to be inert. Now what this means is I can't click on it, I can't interact with it, I can't tap into it. This is basically like a better version of pointer events none. And that's something that people often use, right? They just say, well, I can't click on this area, therefore you can't use it, right? Well, no, as you can see in my example before, it's very easy to tap behind that area. So Inert basically gives you that for free, but also makes the whole anything in the space just unusable. Now this is a very broad primitive, right? Like I'll go through some examples later on, but it's worth thinking about, right? You can build a lot of things with it. One interesting downside is that you can't actually turn it off. So once a DOM tree is inert, that's it. I can't make some subsection un-inert. Now, whether you think that's a feature or a bug, well, that's up to you, but to me, it's kind of a feature, right? If I have a component system and say, well, everything within me is turned off, something within that system can't say, oh no, I really want to be turned on, right? So it turns everything off all the way down. And so the last component I want to talk about is actually quite ancient. It's the field set. Now, you might think of the field set as just some nice way to group input elements together, but what you don't know, or what you might not know, is that you can actually set disabled on the field set. What this does is kind of like a lesser version of Inert. It's going to propagate that disabled down to any HTML form elements within the field set. So links will still be clickable, but like an input or a button will be read-only and disabled. Now, this is actually not as good as Inert, right? It does fewer things. Inert's not quite there yet in Firefox. It's in nightly, which means it's probably going to ship soon, but this is like an interesting fallback that you can use if all your problem is just submitting a form, right? I don't want users to be able to muck with a form while it's in progress, and I don't want to modify every field. I can just wrap it in a field set and say, disabled, off we go. It has one weird nuance with Shadow DOM, but it's possibly a bug more than a feature in that it doesn't propagate through to elements within a Shadow DOM's shadow root. And so one of the reasons I'm excited to talk today is that these three elements just work. Firefox, Safari, and Chrome support nearly all of these features. As you can see, and as I mentioned, Firefox is slightly behind on Inert, but honestly, by the time you watch this talk, it's probably going to be there. You can just start using this stuff. These are primitives, though. You can use them directly. That's fine. You can also think about using them as part of a framework or a library. And I fully expect some libraries to basically have a fast path where they just say, we've got this amazing new feature. We don't need to do our old weird hacks. We can just leverage Inert, Dialog, or even Fieldset, although they might be doing that already. That's quite old. Now, with these amazing new primitives introduced, it's worth talking about how we've gotten here. Now, you know, browsers were written for an ancient time where documents were documents, right? You clicked on links, you could go to other pages, and there was no app-like experiences in sight. But as we've added more functionality to the web and made it this amazing platform to build interesting stuff on, we've needed to control its focus much more distinctly. The tools people have had available over the years are things like literally just removing links, you know, making a button disabled one by one. I mentioned Pointer Events. That came with IE11, and it was sort of a new era in trying to railroad or guide people through your application in like a nice, friendly, organic way. And so the challenges of doing things like Inert or Disabled was that Pointer Events, as I mentioned, can be really trivially escaped, right? I can't just hide an element behind a div. I could manually set elements to input disabled or, you know, disable the href on a link, but you've got to do it for every single one. You know, what if it was already disabled? What if you're in a shadow root? What if all these what ifs, right? And polyfills make this kind of better, but they're horrible hacks, right? And one of the challenges of these things is like I can't really intercept the user's intent or goals globally. Now, these new primitives we have actually let us do that in a much nicer way. And it's also worth calling out that making a dialogue without a built-in primitive is also quite challenging. The biggest one being it's often not clear whether you can hoist yourself to the top of the page. You know, does position fix do what you want at the position you're currently in? And as you saw from the demo before, with the actual built-in dialogue, that's no longer a problem. I can just have a component that makes a dialogue and it will just appear on top, which is pretty much what I always want when I encapsulate elements together. Frameworks, I will say, can help. And if you're building your app entirely within a framework like React, then dialogue may be a solved problem for you. I know that when I write React code, then we use a library that will actually hoist that element to the top level for you automatically, right? Like the fact that it's a dialogue within my React component is sort of irrelevant, right? It gets rendered somewhere else anyway. But some frameworks don't allow that. Some layouts are never going to support that behavior. So having a built-in dialogue primitive is quite lovely. In general, the thing you're trying to avoid is fighting against the browser. As I mentioned, browsers were fundamentally designed for documents, not apps. One thing polyfills try to do is intercept and override every single focus event in totally weird ways. But it's futile. There's always more cases. So now that we've got here and I've given you a bit of a history lesson, let's talk through some of the reasons why you might use these components. Now, Inert and Fieldset are pretty much good for controlling forms and controlling parts of your UI where users shouldn't interact with it after something is done. And so in this example, I've got this quite fancy form. And when I hit submit, yeah, I can block it all out almost immediately and say, don't mark with this. I'm, you know, processing a payment or something like that. I can even overlay a div on here showing my cancel button or some behavior that I really want users to flow through. And what's important here is I don't have to mark with the underlying form, right? I don't have to worry about disabling things. I can just mark the whole thing as inert and create some new interesting state for my users to be in. Now, this is a subtree. Maybe I can do it for a sidebar or a larger part of my app that the users shouldn't interact with for some time for whatever reason. Now, you know, you can go away with this talk and create some interesting experiences. And I've got some ideas here, but I think these are wonderful primitives, right? I think we'll see over the next couple of years, people using them in vastly different ways. So now let's talk about dialogue. And it's worth saying and getting out of the way, I'm not a fan of you using a real modal or even a giant div for blocking interstitials, things like sign up to my newsletter when I really just want to read your content. I think we all as web developers know that's a terrible anti-pattern. But having said that, there are going to be cases where we do need to show a real modal on your webpage. That can be because the user's, I don't know, saving some state that you're confirming, you know, do they want to leave? Are they sure they want to cancel? And in this example here, we've got a pretty good, nice dialogue that shows, you know, the user's type, some document content, but they aren't sure if they're ready to finish or whatever. And so we give them a few options here that make this experience feel quite nice. We've got a close button to get rid of it. We've got a cancel button. And we can use this modal in an interesting way. You know, there's a lot of stuff going on behind the page here and the user doesn't have to worry about it. They can just be focused on one thing for a given amount of time. And modals, you know, really shine in this example. Now, another thing we can do with the dialogue is also animate it. In the end, it's a div, right? It's a fancy div that has some magical properties and it renders in a weird place, but it's a primitive that I can use in any way I like. So when I show a dialogue, I can combine the show modal call with a, perhaps I'm going to start the dialogue off screen and then let it animate it in over a second. One interesting thing I can even do here is maybe disable the interaction on the dialogue with inert during that time. Now, maybe a second is too long, but you get the idea. Similarly, when it closes, yes, there is a built-in close method. Yes, there is an escape handle which I can prevent from happening. And when a dialogue closes, I can not actually close it for the same amount of time. I can say maybe for a short period, it animates out. And once that is done, I then properly actually close it. In the end, it's just a primitive, right? Like I should be wrapping this up behavior up in some other component or helper that does this for me because the close method is not something that a user should necessarily call directly. It's something that you can control. So I'd like to finish up this talk by talking about some other things dialogue can do. I think it's worth being complete, but I will be clear. I think these things are the least interesting part of dialogue. Now, for a better backstory, right? Chrome introduced dialogue in 2014. That's literally eight years ago. And Safari and Firefox have only just come to the table and built that feature out. And that's great, right? That's why the talk is here today. And I can say, please go and use dialogue. It's available everywhere. But I will say some of its weird features were built for kind of a different part of the web. Now, it has non-modal support. I can just toggle this open attribute and dialogue will appear and disappear, but it doesn't block the page. It doesn't render in weird ways. And so it's kind of pointless, right? It's just a div in this regard. It also allows a thing called form method dialogue. Now, this is kind of cute, right? You create a form and when it's submitted, it'll automatically close the dialogue around it. But the reality is, what we do for every other form on the web today, or we want to intercept it with JavaScript or whatever, is we literally overload its submit method and then prevent default. Now, I don't know why form method dialogue exists when I can literally do this to a regular dialogue that has an implicit form method that I just disable or ignore with JavaScript. In fact, I'm already using the dialogue with JavaScript. So it's bizarre to me that we have this weird dialogue type form that sets a return value on the dialogue. Now, you might find it useful, but honestly, I think it's part of a spec that hasn't really aged that well. So I'm only mentioning this stuff for completeness. I don't want to end on a downer, right? Like, the thing to take away from this talk is that dialogue is an amazing primitive that gives you a modal experience that can be used in a bunch of ways, but which exists in the top layer, can be nicely encapsulated and used in components, and hopefully will give you the thing that you've wanted for a long time, but have maybe feared the reams of JavaScript or the horrible hacks to get there. And no, it's very similar. And even field set, even though it's not available because Firefox is a little bit behind the fold, will also give you most of that functionality, right? You can quickly disable forms. And while it sounds like a really silly thing to have to be able to do, it's quite nice to have in your toolbox. So I hope you'll take away and go and run with these ideas. I will say one thing I haven't mentioned extensively is accessibility, or really at all. Now, all these things will kind of do the right thing, but I would encourage you to go have a read if this is important to you about how these things interact with the accessibility tree. I'm not an expert, and I don't think you should just use these features without any knowledge of that. So thanks for listening. I'm on Twitter. You can follow me in a bunch of places. I regularly blog about this kind of stuff. You can see my blog here and, you know, subscribe to it with your favorite RSS reader. And thank you for JS Nation for having me, and hopefully I'll see you around on the internets. Farewell. ♪♪♪
17 min
20 Jun, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic