Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Thu, 23 Oct 2025 19:33:21 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 closedby=”any” https://frontendmasters.com/blog/closedbyany/ https://frontendmasters.com/blog/closedbyany/#respond Thu, 23 Oct 2025 19:33:19 +0000 https://frontendmasters.com/blog/?p=7498 . HTML popovers have this “light dismiss” behavior where you can “click outside” to close them, but not dialogs (until this). I forked a previous demo to try it and it works great (in Chrome & Firefox, just waiting for Safari). I’ve been using a custom <ClickOutsideDetector […]]]> I’m just hearing about the closedby="any" attribute/value for <dialog>. HTML popovers have this “light dismiss” behavior where you can “click outside” to close them, but not dialogs (until this). I forked a previous demo to try it and it works great (in Chrome & Firefox, just waiting for Safari). I’ve been using a custom <ClickOutsideDetector /> element for ages, so this is a welcome feature.

]]>
https://frontendmasters.com/blog/closedbyany/feed/ 0 7498
Move Modal in on a… shape() https://frontendmasters.com/blog/move-modal-in-on-a-shape/ https://frontendmasters.com/blog/move-modal-in-on-a-shape/#comments Thu, 22 May 2025 18:27:26 +0000 https://frontendmasters.com/blog/?p=5917 , as we can do both open & close animations now.]]> Years ago I did a demo where a modal was triggered open and it came flying in on a curved path. I always thought that was kinda cool. Time has chugged on, and I thought I’d revisit that with a variety of improved web platform technology.

  1. Instead of a <div> it’ll be a proper <dialog>.
  2. We’ll set it up to work with no JavaScript at all. But we’ll fall back to using the JavaScript methods .showModal() and .close() to support browsers that don’t support the invoker command stuff.
  3. We’ll use @starting-style, which is arguably more verbose, but allows for opening and closing animations while allowing the <dialog> to be display: none; when closed which is better than it was before where the dialog was always in the accessibility tree.
  4. Instead of path() for the offset-path, which forced us into pixels, we’ll use shape() which allows us to use the viewport better. But we’ll still fall back to path().
  5. We’ll continue accounting for prefers-reduced-motion however we need to.

Here’s where the refactor ends up:

1. Use a Dialog

The <dialog> element is the correct semantic choice for this kind of UI, generally. But particularly if you are wanting to force the user to interact with the dialog before doing anything else (i.e. a “modal”) then <dialog> is particularly good as it moves then traps focus within the dialog.

2. Progressively Enhanced Dialog Open and Close

I only just learned you can open a modal (in the proper “modal” state) without any JavaScript using invokers.

So you can do an “open” button like this, where command is the literal command you have to call to open the modal and the commandfor matches the id of the dialog.

<button
  command="show-modal"
  commandfor="my-dialog"
>
  Open Modal
</button>

You may want to include popovertarget="my-dialog" as well, which is a still-no-JS fallback that will open the modal in a non-modal state (no focus trap) in browsers that don’t support invokers yet. Buttttttttt, we’re going to need a JavaScript fallback anyway, so let’s skip it.

Here’s how a close button can be:

<button
  command="close"
  commandfor="my-dialog"
>
  Close
</button>

For browsers that don’t support that, we’ll use the <dialog> element’s JavaScript API to do the job instead (use whatever selectors you need):

// For browsers that don't support the command/invokes/popup anything yet.
if (document.createElement("button").commandForElement === undefined) {
  const dialog = document.querySelector("#my-dialog");
  const openButton = document.querySelector("#open-button");
  const closeButton = document.querySelector("#close-button");

  openButton.addEventListener("click", () => {
    dialog.showModal();
  });

  closeButton.addEventListener("click", () => {
    dialog.close();
  });
}

At this point, we’ve got a proper dialog that opens and closes.

3. Open & Close Animation while still using display: none;

One thing about <dialog> is that when it’s not open, it’s display: none; automatically, without you having to add any additional styles to do that. Then when you open it (via invoker, method, or adding an open attribute), it becomes display: block; automatically.

For the past forever in CSS, it hasn’t been possible to run animations on elements between display: none and other display values. The element instantly disappears, so when would that animation happen anyway? Well now you can. If you transition the display property and use the allow-discrete keyword, it will ensure that property “flips” when appropriate. That is, it will immediately appear when transitioning away from being hidden and delay flipping until the end of the transition when transitioning into being hidden.

dialog {
  transition: display 1.1s allow-discrete;
}

But we’ll be adding to that transition, which is fine! For instance, to animate opacity on the way both in and out, we can do it like this:

dialog {
  transition:
    display 1.1s allow-discrete,
    opacity 1.1s ease-out;
  opacity: 0;

  &[open] {
    opacity: 1;
    @starting-style {
      opacity: 0;
    }
  }
}

I find that kinda awkward and repetitive, but that’s what it takes and the effect is worth it.

4. Using shape() for the movement

The cool curved movement in the original movement was thanks to animating along an offset-path. But I used offset-path: path() which was the only practical thing available at the time. Now, path() is all but replaced by the way-better-for-CSS shape() function. There is no way with path() to express something like “animate from the top left corner of the window to the middle”, because path() deals in pixels which just can’t know how to do that on an arbitrary screen.

I’ll leave the path() stuff in the to accommodate browsers not supporting shape() yet, so it’ll end up like:

dialog {
  ...

  @supports (offset-rotate: 0deg) {
    offset-rotate: 0deg;
    offset-path: path("M 250,100 S -300,500 -700,-200");
  }
  @supports (
    offset-path: shape(from top left, curve to 50% 50% with 25% 100%)
  ) {
    offset-path: shape(from top left, curve to 50% 50% with 25% 100%);
    offset-distance: 0;
  }
}

That shape() syntax expresses this movement:

Those points flex to whatever is going on in the viewport, unlike the pixel values in path(). Fun!

This stuff is so new from a browser support perspective, I’m finding that Chrome 126, which is the stable version as I write, does support clip-path: shape(), but doesn’t support offset-path: shape(). Chrome Canary is at 128, and does support offset-path: shape(). But the demo is coded such that it falls back to the original path() by using @supports tests.

Here’s a video of it working responsively:

5. Preferring Less Motion

I think this is kind of a good example of honoring the intention.

@media (prefers-reduced-motion) {
  offset-path: none;
  transition: display 0.25s allow-discrete, opacity 0.25s ease-out;
}

With that, there is far less movement. But you still see the modal fade in (a bit quicker) which still might be a helpful animation emphasizing “this is leaving” or “this is entering”.

]]>
https://frontendmasters.com/blog/move-modal-in-on-a-shape/feed/ 1 5917
Lessons Learned from Recreating a Styled Dialog https://frontendmasters.com/blog/lessons-learned-from-recreating-a-styled-dialog/ https://frontendmasters.com/blog/lessons-learned-from-recreating-a-styled-dialog/#comments Wed, 16 Apr 2025 21:54:36 +0000 https://frontendmasters.com/blog/?p=5578 I was on the epicgames.com website the other day, signing up so I could relive my Magic: The Gathering glory days with Arena. While doing that I saw their style for modal dialogs and thought I should try to re-create that with <dialog> because apparently I’m both of those types of nerd.

It’s a <dialog>

This thing came up on top of other content, so that alone makes it appropriate for the HTML <dialog> element. We’ll use that.

No Taller than Viewport

It’s not absolutely required that the entire <dialog> needs to be shorter than the viewport. If it’s taller, you just need to be able to scroll to all of the content it contains. The default styling for <dialog> allows for that.

But I would argue that if you’re putting actions that relate to the content of the dialog at the bottom then you should limit the height of the dialog to the viewport so that those actions are always visible. If a dialog simply has an ✕ close button on the top, maybe it doesn’t matter, but here we’ve got important buttons at the bottom, so it does.

The default styling for dialog includes position: absolute; and we can keep that while limiting the height like:

dialog {
  block-size: 90dvb;
  inset-block-start: 5dvb;
}

That will limit the height to essentially 90% of the viewport height (the dvb part means “dynamic viewport size in the block direction”). I like the “dynamic” sizing units because it means that it accommodates browser “chrome” (toolbars and stuff) being present (or not). The inset amount is half of what’s left over, so essentially vertical centering.

This graphic convinces me dynamic viewport height units are a good idea. (source)

Note that the dialog element’s default styles can be a bit confusing and you need to understand when you can override safely and when you can’t without doing extra work. Simon Willison has an interesting article on this: Styling an HTML dialog modal to take the full height of the viewport.

Limited Width and Centering

This example has lots of written content in it (a bunch of <p>s) so it’s best practice to limit the width to a readable line length. When that’s the intent, it’s nice to use the ch unit as it maps roughly to number of characters, which is what we’re trying to limit.

dialog {
  ...

  inline-size: min(50ch, 90dvi);
  margin-inline: auto;
}

Fifty characters of width is providing good readability here, but it’s possible that the current screen is actually narrower than that, hence the min() function assuring that the width will never be wider than 90% of the viewport. I’m not sure if our fancy dancy “dynamic viewport units in the inline direction” is buying us anything here, but it balances the usage with where we were using dvb).

Modal vs Non Modal (and the open attribute)

This seems like a pretty important distinction to know about:

  • “Modal” (dialog.showModal()) means interrupt everything else, this dialog needs to be dealt with immediately.
    • The ESC key automatically works to close it.
    • Focus is put on the first focusable element within the dialog
    • Focus is trapped within the dialog
  • “Non Modal” (dialog.show()) means the dialog is just there, but doesn’t require exclusive or immediate action.
    • None of those other things above happen. You likely want to bind the ESC key yourself anyway.
    • When you use the open attribute (useful when working on them!) like <dialog open> the dialog is open non-modally.

In our example, where a person needs to accept-or-not the Terms & Conditions, it’s likely modal is the better approach. That way what the person is trying to do can only continue if they accept or take a different path if they do not. This choice is likely required to know what to do next.

A non-modal dialog implementation might be something like a “site navigation drawer” where some of the attributes of using a modal is desirable (e.g. the hide/show behavior) but focus trapping is not required or even desirable.

Here’s a video of focus trapping at work with the modal state. Notice the “focusable element” (an anchor link) never gets focus, because it’s not within the <dialog>.

No Invokers? Yes Invokers!

There is no way to show a dialog in the modal state from HTML alone.

Welllll, the above isn’t strictly true anymore as I learned from Curtis Wilcox in the comments. We can actually use the popover syntax to make a button in HTML alone that will open the dialog. That will (sadly) only open the dialog in the non-modal state, but at least it’s a toggle without JavaScript! The good news is that the Invoker Commands API is actually all over this. It’s used like this:

<dialog id="my-dialog" popover>
  ...
</dialog>

<!-- 
  popovertarget is the fallback

  command attributes are the new school,
  which open in a modal state!
-->
<button
  popovertarget="my-dialog"

  command="show-modal"
  commandfor="my-dialog"
>
  Open Modal
</button>

<button
  popovertarget="my-dialog"
  popovertargetaction="hide"

  commandfor="my-dialog"
  command="close"
>
  Close
</button>

To bone up: What’s the Difference Between HTML’s Dialog Element and Popovers?

This demo has an example of trying to use both invokers and popovers before falling back to the JavaScript methods for opening/closing.

Careful with the display value

The reason that <dialog> is invisible by default is simply that default styles render it with display: none;. That is precariously easy to override. In fact, in this very demo I was playing with, I wanted to use display: flex; on the dialog to have the header/content/footer look where the content is flex: 1; to push the header and footer away and take up the remaining space. But you’ll have problems like this:

/* Oops, dialog is always open */
dialog {
  display: flex;
}

It’s probably most resilient to just not mess with the display value of dialogs, instead using some internal wrapper element instead. But I’m a gamblin’ man apparently so I did:

dialog {
  &[open] {
    display: flex;
  }
}

Trimming margin can come anytime now

Any time I slap a bunch of elements into a container (read: doing web design) I’m reminded that the block-direction margins are kind of annoying in that context. The last item, particularly if it’s content, will likely have margin at the end that pushes further than you want it away from the container, or the start, or both.

It leads to this kind of thing:

.container {
  :first-child {
    margin-block-start: 0;
  }
  :last-child {
    margin-block-end: 0;
  }
}

When instead we could be living in the future like:

.container {
  margin-trim: block;
}

I once said this and I’m sticking to it:

If you add padding in the main flow direction of an element, adding margin-trim in that same direction.

Right aligned buttons deux façons

I had item-flow on my brain when I was tinkering with this and thinking about how flow directions can be reversed, which is something I don’t think about or use very much. For some reason when I needed to right-align those buttons for “Accept” and “Close”, my fingers went for:

dialog {
  > footer {
    display: flex;
    flex-direction: row-reverse;
  }
}

I’m not going to recommend that, as it changes the tabbing order awkwardly for no great reason. You should probably just do:

dialog {
  > footer {
    display: flex;
    justify-content: end;
  }
}

But, ya know, always nice to have options. You could also not even bother with flex and do text-align: end or even old school float: right the buttons.

Autofocus

In reading over the MDN for dialogs, this stood out to me as something I didn’t know:

The autofocus attribute should be added to the element the user is expected to interact with immediately upon opening a modal dialog. If no other element involves more immediate interaction, it is recommended to add autofocus to the close button inside the dialog, or the dialog itself if the user is expected to click/activate it to dismiss.

They didn’t mince words there and it makes sense to me, so I put it on the “Accept” button as that seems like the most likely user action.

<dialog>
  ...
  <footer>
    ...
    <button autofocus>Accept</button>
  </footer>
</dialog>

Feel free to peak at the dxemo to see a few other thing like color modes and a backdrop. Sometimes fairly simple looking HTML elements have quite a bit of detail to implementation!

]]>
https://frontendmasters.com/blog/lessons-learned-from-recreating-a-styled-dialog/feed/ 5 5578
Relatively New Things You Should Know about HTML Heading Into 2025 https://frontendmasters.com/blog/bone-up-html-2025/ https://frontendmasters.com/blog/bone-up-html-2025/#comments Mon, 06 Jan 2025 15:42:42 +0000 https://frontendmasters.com/blog/?p=4732 Not all of this is like absolutely brand spanking new just-dropped-in-2024 stuff. Some of it is, but generally it’s relatively new stuff that’s all pretty great. I’m pointing things out that I think are really worth knowing about. It’s possible you haven’t kept up too much with HTML developments as it tends to, rightfully, move a lot slower than CSS or JavaScript.

A group of details elements can behave like an accordion, among other improvements, but still have accessibility limitations.

We’ve had cross-browser <details> / <summary> support since 2016, but only recently have the abilities started to expand and clean up.

For one, you can make an “exclusive” accordion by grouping them together via the name attribute:

<details name="group"><summary>One</summary> ... </details>
<details name="group"><summary>At</summary> ... </details>
<details name="group"><summary>A</summary> ... </details>
<details name="group"><summary>Time</summary> ... </details>

Me, I mostly think the only-one-open-at-a-time thing is an anti-pattern (as do others), mostly because it’s weird to close something a user may have intentionally opened via side effect. But the web is a big place and certain specific designs I can see needing it to be effective so I don’t hate that it exists. At least I think using the term “accordion” is now appropriate in this context, but that there are still potential accessibility issues. Like imagine using this for a FAQ section where each question would normally be a header like <h3>, well, the semantics of that <h3> is wiped out by the <summary>, which is a “button”, so that’s not great.

Here’s an example of the accordion pattern being used with a group of horizontally laid out details elements. If more could be opened, it could cause horizontal scroll which I sure we can all imagine also sucks.

Note that those <details> elements are in a flexbox layout and are themselves display: flex; and only recently has that improved. (See Stephanie Stimac’s article on recent improvements.)

Ya know how the inside of a <details> is:

  1. <summary>
  2. … and whatever else

The “whatever else” can be one or more elements, and there isn’t any particularly clean way of selecting them. This is a CSS thing, but now we’ve got a ::details-content pseudo-element selector to get our hands on all that HTML in case it needs similar treatment (imagine fading it all in or out).

Here’s a great demo of using that, as well as other brand new CSS features, to make honest-to-god animating open/close details elements with arbitrary content in just HTML and CSS.

Styleable Selects are Coming

Browsers can’t just all the sudden make every aspect of a <select> and <option>s styleable, otherwise historical CSS that didn’t apply to them all the sudden does and it would wreak untold havoc on websites. The web doesn’t like to roll like that, and I applaud it for that backwards compatibility.

So… there needed to be an opt-in to make it work. A new element can work for that, which for a hot minute seemed like it would be <selectmenu>. But the progressive enhancement story for that basically sucked. So the new opt-in looks like it will be CSS triggered:

select,
::picker(select) {
  appearance: base-select;
}
Demo

Once you’ve opted in, you can apply styling to elements inside the <select> pretty freely, opening up huge doors to designing that experience.

There is some other funky things to know so I’d suggest reading this whole article. Even some new (tricky) HTML!

<select class="country-select">
  <button>
    <selectedoption></selectedoption>
  </button>
  <option value="" hidden>
    <figure></figure>
    <span>Select a country</span>
  </option>
  <option value="andorra">
    <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Flag_of_Andorra.svg/120px-Flag_of_Andorra.svg.png" alt="" />
    <span>Andorra</span>
  </option>
  ...
</select>

My favorite episode of Off the Main Thread this year was about styleable selects and all the interesting details behind them that will have knock-on effects.

Oh — also you can slap a line into a select menu

I kinda love how the old school name attribute was used with <details> for the accordion behavior.

And speaking of old school elements, you can put an <hr> within a <select> to just draw a line there (a “horizontal rule” as it were). You’ve still got <optgroup label="label"> for more emphatic grouping, but sometimes a line is all you need.

<select>
   <option>apple</option>
   <option>orange</option>
   <option>banana</option>
   <hr>
   <option>pepper</option>
   <option>squash</option>
   <option>broccoli</option>
</select>

You Can Open/Close a Popover with a Button Alone

No JavaScript is required to make the opening and closing of a popover work.

<button popovertarget="the-popover">Open Popover</button>

<div popover id="the-popover">
  I'm a popover.
  
  <button popovertarget="the-popover">Close Popover</button>
</div>

If you’d prefer that the popover be closed by just a click anywhere outside of it (that’s called a “light dismiss”) then update the popover attribute to popover="auto" and that’ll do it.

The “targetting” you can see happening with those buttons is an example of an “Invoker”, which is poised bring great power to HTML in coming years.

You can’t close a popover with a form submission like you can a <dialog>, but a popover probably isn’t a great place for a form anyway so that’s fine. Another interesting sidenote is you can make a <dialog popover> if you like, getting you this button behavior for free.

There are quite a few differences between dialogs and popovers, and both are awfully useful. Perhaps the most important two features being the focus trap potential and the fact they are promoted to the “top layer” of the rendered site, meaning no futzing with z-index.

The situation we’re in with popovers is that you pretty much need to be OK with either centered or edge-based positioning for them for now, like dialogs. They are just begging for anchor positioning, but the current guess is 2026 for interop on that.

Checkboxes can be Toggle Switches

It’s as easy as:

<input type="checkbox" switch>

Although only Safari supports it for now and it’s not actually specced yet so it could be said they jumped the gun a bit. Main discussion here. And I guess we should call it a “switch” to be proper.

I’m a fan here because the absolutely correct implementation of a toggle/switch was easy to get wrong from an accessibility standpoint, and this seems… hard to get wrong.

Daniel Yuschick has an article digging into the details. I like the idea that pseudo elements specific to this UI will be exposed, like ::thumb and ::track, but I can’t tell you what the status of that is right now. Even the official demos in Safari Nightly Preview with the flag turned on aren’t rendering properly for me.

Wrap your Search

This will be easy to remember. Got an area of your site that is meant for searching? Wrap it.

<search>

</search>

It’s the same as doing <div role="search">, but my bet is that you’ll actually remember to do it.

You probably don’t need noopener noreferrer on links anymore

I’ve used linters on projects that help ensure that a link like:

<a 
  href="https://google.com" 
  target="_blank"
>

Has attributes like this as well:

<a 
  href="https://google.com" 
  target="_blank"
  rel="noopener noreferrer"
>

The problem was, that actually gave the opened pages rights to their referrer: it opened a security hole that could potentially have leaked user information or opened the door to phishing.

Ben Werd

This is not new “2025” information, but I’m only just learning that this isn’t really needed anymore. Chrome was the last to automatically apply this behavior to _blank links and that was 2021. I’ve been doing it as I have a linter that always warns me about it, so in case your browser support targets agree, you might want to check those linter settings.

Declarative Shadow DOM Paves the Way for Better Web Component Frameworks

It used to be that if you wanted a Web Component to use Shadow DOM, the only way to do it was for it to be rendered by JavaScript. This meant that Web Components that wanted or needed to use Shadow DOM had no Server Side Rendering (SSR) story at all. That was a big gap, as all the major UI frameworks have coalesced on the idea that SSR is a good idea for many reasons (performance (perceived and actual), resilience, SEO, hosting options, etc).

Now we’ve got Declarative Shadow DOM and the gap has closed.

I think it’s cool to see the Shadow DOM at work with no JavaScript at all:

Demo

What I hope we’ll see in 2025 and beyond is frameworks actually help use this. It feels like foundational technology that mostly isn’t expected to be written by hand by authors, but instead used by libraries/frameworks to build great authoring experiences around.

React 19 was the last framework to fully correctly support Web Components, so perhaps we’ll see frameworks do more than support them now but embrace them. I would expect to see a “Next.js of Web Components” at some point.

Import Maps

I used to be fond of point out that a line like this isn’t standard JavaScript.

import React from "react";

That looks like ES Modules code, but the fact that the value in quotes doesn’t start with an absolute URL or a . (a relative path) means it’s… not. It’s just a convention that we all got used to writing because JavaScript bundlers understand it to mean “that’s a thing from npm so I should go look for it in the node_modules folder.

That’s changed now, since you can, via HTML Import Maps, map the value “react” to something else via Import Maps.

So if you executed that JavaScript above from an HTML file that included an import map like this:

<script type="importmap">
  {
    "imports": {
      "react": "https://esm.sh/react@18",
      "reactdom": "https://esm.sh/react-dom@18"
    }
  }
</script>

(Basic demo.)

It would work and import the JavaScript from those locations instead. This opens up the door for not needing to use a bundler and still having the benefit of an abstraction for importing dependencies. Having the possibility to avoid tooling can be huge for long term maintenance of projects.

Don’t Sleep on the inert Attribute

You can make an element, and the entire chunk of DOM under it, ignored completely from an interactivity perspective, just by by using the inert attribute. It’s quite powerful. No clicks, no focus, the element(s) are gone from the accessibility tree, nothing can be selected, heck, the on-page “find” command won’t even find text within there.

If you’ve got any reason at all to put stuff in the DOM but have it essentially behave as if it isn’t there, make it inert until you are ready for it not to be. Imagine a multi-step form that is all in the DOM right away, but only one step at a time is not inert, so future or previous form controls aren’t accidentally used until ready.

I’d tell you this is ideal for implementing modals, but you get this behavior for free, and easier because it can be placed anywhere, just by using <dialog>.

Keep your find-on-page working properly

Another interesting attribute here. We’ve long had hidden as an attribute (even though it’s kinda weak). The change here is it taking a value, like hidden="until-found". That will hide the element as hidden does, but the content within it will still be findable with on-page text search. When it is found, it’s on you to react to the DOM event beforematch to un-hide (by removing the attribute) the content so it can be seen the hidden attribute is removed automatically, plus you’ve got the beforematch event to hook into if you need to do additional work (thx Nathan).

Here’s the demo from chrome for developers, which you might need to try in Debug View for it to trigger properly.

You’d think this would be useful for <details> elements in how they hide content, but this behavior is baked into them. This is more solid evidence for using native features — because they get the details right (generally) rather than you needing to replicate them.

Multi-Page View Transitions

You have to opt-in to this via CSS like:

@view-transition {
  navigation: auto;
}

Then once you have, regular ol’ clicked links that move to a new page (on the same domain) can have view transitions. That is, you’ll see the page use a fade effect by default. But this unlocks an amazing amount of animation control over the transition between those two page loads.

I’m listing this as an HTML feature, because I find most of the useful-ness of multi-page view transitions are unlocked in the HTML. For instance…

<!-- homepage -->
<div class="card">
  <h3 
    style="view-transition-name: post-title-087afd;"
  >Blog Post Title</h3>
  <p>...</p>
  <a href="/blog-post">Read the article Blog Post Title</a>
</div>

<!-- blog post -->
<article>
  <h1 
    style="view-transition-name: post-title-087afd;"
  >Blog Post Title</h1>
  <p>...</p>
</article>

Above, I’m imagining the “card” as a component that is generated from data and is likely one of many on the page. So it requires having a unique view-transition-name, and really the only good opportunity to apply that is in HTML.

Responsive Video is catching up to Responsive Images

With the <picture> element in HTML we get the ability to use the <source> element within and control exactly when we swap out to different source images. It’s a powerful concept that can offer big performance wins.

That idea actually originally came from a concept with the <video> tag, but then that was bizarrely removed from (most) browsers. But now it’s back thanks to some serious work by Scott Jehl and others he convinced to help the revival along.

You can do media attribute control the sources, which will probably mostly be used for width query stuff, but it can do anything media can do. Scott wrote up some examples here, like:

<video autoplay controls playsinline muted loop>
  <source media="(orientation: landscape)" src="sunset-landscape-1080.mp4">
  <source src="sunset-portrait-1080.mp4">
</video>

HTML Imports are Coming Back

Just kidding they totally aren’t.

Did we miss anything?

Any newfangled HTML out there you’ve been eyeing up or using?

]]>
https://frontendmasters.com/blog/bone-up-html-2025/feed/ 17 4732
What’s the Difference Between HTML’s Dialog Element and Popovers? https://frontendmasters.com/blog/whats-the-difference-between-htmls-dialog-element-and-popovers/ https://frontendmasters.com/blog/whats-the-difference-between-htmls-dialog-element-and-popovers/#respond Mon, 30 Sep 2024 16:38:20 +0000 https://frontendmasters.com/blog/?p=4069 They are different HTML, to begin with. A dialog is like this:

<dialog id="my-dialog">
  Content
</dialog>

While a popover is an attribute on some other element:

<aside popover id="my-popover">
  Content
</aside>

The reason it’s worth comparing them is that they are quite similar in a lot of ways, both in look and functionality, which can be confusing. It’s worth thinking about which one you really need.

They are both hidden-by-default

If you put either bit of the HTML above onto the page, they will be visually hidden as well as ignored in the accessibility tree by default (but available in the DOM). It isn’t until you specifically show them (via JavaScript or on-page HTML control when available) that they are visible.

Accessibility tree with a hidden dialog and popover in it.
When dialog is open, it’s a part of the accessibility tree.

You can make a <dialog> visible by default in HTML alone:

<dialog
  id="my-dialog"
  open
>
  Content
</dialog>

Where you cannot make a popover visible in HTML alone.

Popovers Have HTML-Only Controls

You can make a popover work (open & close) with HTML controls alone:

<!-- This button will open and close the matching popover. No JavaScript required. -->
<button popovertarget="my-popover">
  Toggle Popover
</button>

<aside popover id="my-popover">
  Content of popover
</aside>

But you cannot build HTML-only controls for a <dialog>. Opening and closing a dialog requires JavaScript event handlers.

JavaScript APIs

Dialog JavaScript APIs

The dialog APIs in JavaScript are interesting in that there are two different distinct APIs for opening it. This is where the term “modal” comes in. Modal is sometimes used as a term for the UI element itself, but here it essentially means if the modal should trap focus inside of it while open, or not.

  • .show() — Open the dialog in a non-modal state, meaning no backdrop is shown and no focus trapping happens. Note that using the open attribute in the HTML/DOM to open the dialog is the same (non-modal).
  • .showModal() — Open the dialog in a modal meaning a backdrop is shown and focus is trapped within the modal.
  • .close() — Closes the dialog (if it’s open).

The showModal() method can throw if the dialog is already open in a non-modal state.

Uncaught InvalidStateError: Failed to execute 'showModal' on 'HTMLDialogElement': The dialog is already open as a non-modal dialog, and therefore cannot be opened as a modal dialog.

Popover JS APIs

Popovers also have JavaScript APIs, but both the opening and closing APIs are different than with modals and do not overlap. These are pretty self explanatory.

  • .showPopover() — Opens the popover.
  • .hidePopover() — Closes the popover.

Calling showPopover on an already open popover or hidePopover on an already hidden popover does not throw.

Focus Trapping

The ability of the dialog element to be opened in a modal state and thus trap focus inside of it is a superpower of this element. It is unique to the dialog element, popovers cannot do this (on their own).

Focus trapping, while it sounds kinda bad, is actually an accessibility feature. After all, that’s what a modal is: it forces you to deal with some interaction before anything else can be done. It’s actually also a WCAG requirement to not trap focus when you shouldn’t, but in the case of a modal, you should be trapping focus — as well as providing a standard way to close the dialog and escape the trap.

Focus can change to other focusable elements inside, and when you’re about to move focus forward to the next element when you’re at the last, it circles back to the first focusable element within the dialog. You get all this “for free” with a <dialog> opened with showModal(), which is otherwise a huge pain in the ass and you probably won’t even do it right (sorry).

If you need this focus trapping, don’t use a popover as it’s not for this job.

Moving Focus

When a dialog is opened (either modal or non-modal), focus is moved to the first focusable element within it. When it is closed, focus is moved back to the element that opened it.

With a popover, focus remains on the element that opened it even after the popup is opened. However, after the popup is open, the next tab will put focus into the popup’s content if there is any in there, regardless of where it is in the DOM, tab through the focusable elements of the popup, then onto other focusable elements outside the popup after the original element that opened it.

This is all tricky work that you get for free by using the <dialog> element or popups and frankly a huge reason to use them 👍.

Escape Key

Both modal dialogs and popups, when open, can be closed by pressing the ESC key. Very handy behavior that helps adhere to accessibility adherence, again given for free, which is tricky and error-prone to write yourself.

Non-modal dialogs do not close with the ESC key, so you’ll need to provide your own close functionality, like:

<button onclick="myDialog.close()">Close</button>

They Have the Same Default Styling

Dialogs and popovers look the same by default and have really basic default styling that you’ll almost certainly want to override.

They are essentially position: fixed; and margin: auto; which centers them in the viewport. This is a probably a smart default for dialogs. In my opinion, popovers are usually begging for anchor positioning to open the popover near where the element that opened it is, but they work nicely as slide-out drawers as well, particularly on mobile.

You’ll likely want to bring your own padding, border, background, typography, internal structure, etc.

The Top Layer

Another amazing feature of both dialogs and popovers is that, when open, they are placed on what is called the “top layer”. It is literally impossible for any other element to be on top of them. It doesn’t matter where they are in the DOM (could be quite nested) or what containing blocks or z-index is involved, the top layer is the top no matter what. (Although – it is true that if you open subsequent dialogs/popovers, e.g. a button in a dialog opens another dialog, the second one will beat the first and be on top, as you’d expect.) This top-layer ability is yet another thing you get for free and a fantastic reason to use these native features.

DevTools showing the #top-layer

Backdrops

Both (modal) dialogs and popovers use (and share) a backdrop. This is the layer above all content on the page that covers the page (by default), but is still underneath the actual dialog or popover. This backdrop is a very light transparent black by default, but can be styled like this:

::backdrop {
  background: color-mix(in srgb, purple, transparent 20%);
}

That will apply both to modal dialogs and default popovers. If you wanted to have different backdrops for them, you could scope them like this, as the backdrop is applied to the element that is open:

[popover]::backdrop {
  
}

dialog::backdrop {
  
}

.some-very-specific-element::backdrop {

}

You don’t have to show a backdrop if you don’t want to, but it’s a good indicator for users particularly when modal behavior is in play (and perhaps an anti-pattern when it’s not, as you may be visually hiding elements in focus).

Non-modal dialogs do not have a backdrop.

Soft Dismiss

This feature is unique to popovers. You can “click outside” the popover to close it, by default (yet another tricky behavior to code yourself). I’ve used the term “default popover” in this article and what I mean is when you don’t provide a value to the popover attribute. That implies auto as a value which is what makes soft dismissal work.

<!-- Soft Dismissible -->
<div popover id="myPopover"></div>

<!-- Soft Dismissible -->
<div popover="auto" id="myPopover"></div>

<!-- NOT Soft Dismissible -->
<div popover="manual" id="myPopover"></div>

Multiple Open

Both dialogs and popovers can have multiple open at once. The most recent one to be opened will be the one that is most “on top” and will close the first via soft dismiss or the ESC key. (Also see the CloseWatcher API).

For a modal dialog, note that because the rest of the page is essentially inert when it is open, the near-only way to open another is via interactivity within the first opened dialog.

For popups, because the default behavior has soft dismissal, the popovers will need to be popover="manual" or be opened with JavaScript without interaction for multiple of them to be open.

Purpose and Semantics

Popovers likely have more use cases than dialogs. Any time you need a tooltip or to provide more contextual information that has good reason not to be visible by default, a popover is a good choice.

Non modal dialogs are pretty similar to a popup, but are perhaps better suited to situations where there is no other element on the page that is relevant to the messaging. Perhaps something like a “No internet connection detected” message, which could be very important to tell a user, but doesn’t need to 100% stop other activity on the page.

Modal dialogs are show-stoppers, forcing a user to deal with them before anything else can happen. They should be used sparingly (they are reached for far too much, some people say). Perhaps a message like “Are you sure you want to delete this entire document? This cannot be undone.” would be a modal dialog, as any other interaction on the page is moot should the user be deleting.

Animation

This is all very cutting edge right now, so browser support is spotty, but both of these elements can be animated both on the way in and out.


I played around with this Pen while I was thinking and working on all this, which may be helpful to you if you’re doing the same.

]]>
https://frontendmasters.com/blog/whats-the-difference-between-htmls-dialog-element-and-popovers/feed/ 0 4069
“Why does this have to be a modal?” https://frontendmasters.com/blog/why-does-this-have-to-be-a-modal/ https://frontendmasters.com/blog/why-does-this-have-to-be-a-modal/#respond Tue, 17 Sep 2024 15:51:50 +0000 https://frontendmasters.com/blog/?p=3873 It’s not that modals are all automatically bad, it’s that, as Adrian Egger says, “modals are the crutch of the inarticulate design and developer” and they “are easily replaced with other patterns that are less jarring.” on the dedicated site for this crusade: modalzmodalzmodalz.com. Adrian’s personal site is sweet, too.

]]>
https://frontendmasters.com/blog/why-does-this-have-to-be-a-modal/feed/ 0 3873
The Dialog Element with Entry *and* Exit Animations https://frontendmasters.com/blog/the-dialog-element-with-entry-and-exit-animations/ https://frontendmasters.com/blog/the-dialog-element-with-entry-and-exit-animations/#respond Wed, 28 Aug 2024 17:11:01 +0000 https://frontendmasters.com/blog/?p=3559 Una Kravets blogged the other day that animating entry effects are now supported in the latest stable version of all major browsers. The new cool way to do it, that is. We’ve long had trickery like applying a @keyframe animation with a to frame that would behave like an “entry effect”, but it was a bit awkward and didn’t work in all situations. Specifically one like using the new and very useful <dialog> element.

This bit of code says a lot:

dialog[open] {
  transition: 
    translate 0.7s ease-out, 
    display 0.7s ease-out allow-discrete;

  /* Post-Entry (Normal) State */
  translate: 0 0;

  /* Pre-Entry State */
  @starting-style {
    translate: 0 100vh;
  }
}

There are two big things at work there:

  1. The display property is listed in the transitions, with the keyword allow-discrete. The code for it is hidden in User-Agent stylesheets, but when a <dialog> moves from close (default) to open, the display goes from none to block. Using this keyword means that the display property is changed after the animation timing, so animations can actually happen.
  2. The @starting-style gives us an opportunity to apply styling to the element just as it’s entering it’s current state, meaning the transition will happen between the styles declared inside and outside that block.

Golf clap. Everything is awesome.

What Una didn’t cover, on purpose surely, was exit animations (because they aren’t in “Baseline” yet, meaning not supported across browsers). But they are supported in Chrome-n-friends land, so I thought it was worth looking at. To me, they are just as interesting, cool, and useful as the entry kind.

Both Entry and Exit

The trick isn’t terribly different than the code above, it’s just to have very specific styles for both the open and closed (i.e. :not([open]) states. Like this:

dialog {
  --duration: 0.34s;

  transition: 
    translate var(--duration) ease-in-out, 
    scale     var(--duration) ease-in-out,
    filter    var(--duration) ease-in-out,
    display   var(--duration) ease-in-out allow-discrete;

  &[open] {

    /* Post-Entry (Normal) State */
    translate: 0 0;
    scale: 1;
    filter: blur(0);

    /* Pre-Entry State */
    @starting-style {
      translate: 0 8vh;
      scale: 1.15;
      filter: blur(8px);
    }
  }

  /* Exiting State */
  &:not([open]) {
    translate: 0 -8vh;
    scale: 1.15;
    filter: blur(8px);
  }
}

Check it out:

And a video in case you’re in a browser that doesn’t support it yet:

Note that not only does it have entry and exit animations, but those states are different — which is very cool! Emphasizing that, here’s one where I move the dialog along an offset-path so the exit is really a continuation of the path:

Usage with Popovers

This isn’t exclusively for dialogs, you can make it work with whatever. But naturally open-closable things make the most sense. Like native popovers! Nils Riedemann has a nice demo here:

]]>
https://frontendmasters.com/blog/the-dialog-element-with-entry-and-exit-animations/feed/ 0 3559
CloseWatcher https://frontendmasters.com/blog/closewatcher/ https://frontendmasters.com/blog/closewatcher/#respond Tue, 27 Aug 2024 18:39:43 +0000 https://frontendmasters.com/blog/?p=3669 I’m first hearing about the CloseWatcher API after running across Abdelrahman Awad’s blog post about it. The MDN docs are quite direct, making the purpose clear:

Some UI components have “close behavior”, meaning that the component appears, and the user can close it when they are finished with it. For example: sidebars, popups, dialogs, or notifications.

Users generally expect to be able to use a particular mechanism to close these elements, and the mechanism tends to be device-specific. For example, on a device with a keyboard it might be the Esc key, but Android might use the back button.

So rather than bind an closing action to the Esc key, use this API, which normalizes that user action across platforms.

I also like how it automatically creates a “stack” of things to close. So if there are multiple closeable things open, a close event only closes the most recently open one. I made a demo to play with this (note the Chrome-n-friends only support!).

A video in case it doesn’t work on your browser:

]]>
https://frontendmasters.com/blog/closewatcher/feed/ 0 3669
Animating the Dialog Element https://frontendmasters.com/blog/animating-dialog/ https://frontendmasters.com/blog/animating-dialog/#comments Thu, 23 May 2024 14:41:24 +0000 https://frontendmasters.com/blog/?p=2341 When the <dialog> element became widely available in 2022, I was thrilled. Opening a dialog? Easy. Closing a dialog? Even easier. Nested dialogs and keyboard interactions? Built-in, for free. It’s like living in the future.

But what about animating? That’s a little trickier. At first glance it doesn’t appear to be animatable in CSS—transitions and animations don’t seem to work. JavaScript can do it, but that requires managing the state of your dialogs manually, losing some of the simplicity of using <dialog> in the first place.

Fortunately, thanks to modern CSS, we can do it without resorting to JavaScript.

Here we’ll take a look at opening and closing animations separately, discussing solutions using transitions and animations for each.

To keep my code simple I’ll stick to only animating opacity, though these techniques still apply to more complex examples.

The nice thing about only animating opacity is we don’t have any extra accessibility concerns. If you’re involving some form of motion in your animations, you’ll need to ensure the relevant code is wrapped in a media query like:

@media (prefers-reduced-motion: no-preference) { }

Opening Animations

Transition with @starting-style

You might have tried something like this, only to find it doesn’t work:

dialog {
  transition: opacity 1s;
  opacity: 0;
  
  &[open] {
    opacity: 1;
  }
}

The problem here is when a <dialog> opens, the browser doesn’t know what opacity value it’s meant to transition from. The first style update our <dialog open> receives sets opacity: 1 , and since that’s also our end value, no transition takes place. We see this problem pop up whenever we attempt to transition any element that changes to or from display: none. How do we fix this?

One way is with @starting-style, an at-rule that allows us to specify the values we’d like to transition from when the element is first rendered.

We can nest it directly in our existing [open] rule like so:

dialog {
  transition: opacity 1s;
  opacity: 0;
  
  &[open] {
    opacity: 1;
	  
    @starting-style {
      opacity: 0;
    }
  }
}

Success! That’s all it takes, our <dialog> will now transition opacity while opening.

We can think of @starting-style as a third state for our dialog, the ‘pre-open’ state. Often we’d want this to be the same as our ‘closed’ state, and while this might seem like an annoying bit of duplication, it’s useful that we can define it separately as it allows our opening and closing transitions to be different.

The downside here, at least at the time of writing, is browser support. @starting-style isn’t in Firefox, and only in recent versions of Chromium and WebKit based browsers. Depending on your requirements that can easily be good enough since:

  1. We’re using @starting-style as a progressive enhancement. In non-supporting browsers the dialog will simply open with no transition.
  2. @starting-style is an Interop 2024 target, so we can expect cross-browser support by the end of the year.

So what if we need a cross-browser opening animation right now? Are we out of luck? Fortunately not.

Animation with @keyframes

By using @keyframes we can get the same effect with browser support limited only by <dialog> itself and remove the need to use @starting-style:

dialog[open] {
  animation: open 1s forwards;
}

@keyframes open {
  from { opacity: 0 }
  to   { opacity: 1 }
}

That’s all we need! We solve the problem of the browser needing to know what initial value to use by explicitly declaring it within the animation.

@keyframes debatably has a few downsides, mostly notably its need for a unique name. That doesn’t sound like a big deal, but naming things can be hard, and name conflicts can be confusing to debug. All else being equal, a technique requiring a unique name is worse than a technique that doesn’t.

Personally however, until @starting-style has near universal support, this will remain my preferred technique. In my opinion it’s equally readable, rarely more verbose, and the fact it works everywhere makes me (and my clients) happy.

Closing Animations

Unfortunately when our <dialog> closes, we run into a few more problems:

  1. It changes to display: none.
  2. It’s removed from the top layer.

Both of these things happen as soon as the close event is fired, and since they both hide our element, any animations or transitions we attempt won’t be visible. We’ll need to delay these while our animation completes, and we can do it in one line with CSS:

transition:
  display 1s allow-discrete,
  overlay 1s allow-discrete;

There’s a few new things in this one declaration, so let’s expand on each of them.

transition-behavior: allow-discrete

Usually when attempting to transition discrete properties we see it doesn’t work, or more accurately, the property’s value updates at 0%, causing an instant change with no transition.

What transition-behavior: allow-discrete usually does is allow us to request that this change occur at 50% of the way through the transition, rather than 0%. I say usually, because for transitions that involve display: none, this change will instead occur at either 100% or 0%, based on if we’re animating to or from display: none. This ensures that our element will remain visible for the entire duration of the transition. Problem #1 solved.

Since the value changes at the beginning or end of the transition, it doesn’t matter what value we use for animation-timing-function so feel free to omit it from the shorthand.

transition-behavior is currently not available in Firefox or Safari, but as it’s also an Interop 2024 target along with @starting-style, we can be optimistic that it’ll be widely available by the end of the year.

It’s also not available in a non-American spelling, so make sure you leave out the ‘u’.

The overlay Property

The overlay property has two possible values: auto and none, and it specifies if an element in the top layer should be rendered in the top layer. Very simply, an element with overlay: auto will render in the top layer and be visible, and an element with overlay: none will not.

What complicates this slightly is that the overlay property is fairly unique in that it’s not possible for you to set it yourself. You can’t set it directly on an element, or use it in a @keyframes animation. The only one who can change the value of this property is the browser. Using it in a transition in combination with allow-discrete is actually our only way of interacting with it at all.

This is also another property that transitions differently than normal discrete properties where it’ll remain overlay: auto for the entire transition. Exactly what we need to solve problem #2.

The overlay keyword is our only method of keeping an element in the top layer, so any CSS only solution to <dialog> closing animations will require it. Unfortunately it’s currently only available Chromium at the time of writing, and since it’s not an Interop 2024 target, we might be waiting a little longer for cross-browser support.

Closing Transition

Lets combine this with our previous example using @starting-style by adding to our existing transition declaration:

dialog {
  transition:
    display 1s allow-discrete,
    overlay 1s allow-discrete,
    opacity 1s;
  opacity: 0;
  
  &[open] {
    opacity: 1;
	  
    @starting-style {
      opacity: 0;
    }
  }
}

And with that we have a <dialog> with both opening and closing transitions! If you’re looking for the simplest solution then you can stop here, it doesn’t come easier than this.

Closing Animation with @keyframes

If you’re like me and want to take advantage of CSS animations to provide a cross-browser opening animation, we’ll need to do a bit more.

It’s possible to use our transition only code to handle the closing animation while keeping @keyframes for our opening animation. But if you’re like me, you might find it a bit easier to understand if both animations are controlled via keyframes.

Since both display and overlay are set by the browser, we still need to transition these values outside of our animations:

dialog {
  transition:
    display 1s allow-discrete,
    overlay 1s allow-discrete;
							
  &[open] {
    animation: open 1s forwards;
  }
}

While I find it a little weird to be using both animation and transition, I like that our animation code is kept separate from our management of the browser’s default behaviour.

We need to ensure our animation-duration is at least as large as our transition-duration to ensure neither overlay or display change before the end of our animation.

Next up is the closing animation itself.

My first instinct was to reuse the same animation but play it in reverse. Unfortunately we can’t do that since it’s not possible to change animation-direction without also starting a new animation with a different name.

Instead, lets define a new set of @keyframes for our closing animation and apply it to the default (closed) state:

dialog {
  transition:
    display 1s allow-discrete,
    overlay 1s allow-discrete;
	
  animation: close 1s forwards;					
  &[open] {
    animation: open 1s forwards;
  }
}

@keyframes open {
  from { opacity: 0 }
  to   { opacity: 1 }
}

@keyframes close {
  from { opacity: 1 }
  to   { opacity: 0 }
}

And that’s all it takes! A <dialog> with a cross-browser opening animation and a progressively enhanced closing animation. It’s a little less concise with a bit more duplication than our transition only example, but you can decide if the extra browser support is worth it for you.

Conclusion

It’s honestly quite amazing how little CSS is required to make this happen. Tools like <dialog>, overlay and transition-behavior have taken what was once an incredibly complicated task and reduced it to just a few lines of CSS.

Dialogs are easier than they’ve ever been, and as long as we don’t get tempted to over use them, that’s cause for celebration to me 🎉

What about popover and ::backdrop?

I kept my explanation focused on the <dialog> element to keep things simple, but everything we’ve just covered also applies popover elements and ::backdrop too! They exist in the top layer and have their display toggled by the browser in the same way <dialog> does, so can be animated using these same techniques.

Here’s Adam Argyle with a snippet that handles popovers and backdrops also, just note it’s using @starting-style so support will be limited for now:

]]>
https://frontendmasters.com/blog/animating-dialog/feed/ 4 2341
Menus, toasts and more with the Popover API, the dialog element, invokers, anchor positioning and @starting-style https://frontendmasters.com/blog/menus-toasts-and-more/ https://frontendmasters.com/blog/menus-toasts-and-more/#respond Mon, 04 Mar 2024 20:21:03 +0000 https://frontendmasters.com/blog/?p=1104 Dropdowns, menus, tooltips, comboboxes, toasts — the popover attribute will make building a large variety of UI components easier. The popover attribute can be used on any HTML element, so you have the flexibility to choose whichever element is most appropriate semantically for each particular use case. Unlike a dialog, a popover is always non-modal — meaning they don’t block interaction with anything else on the page. To toggle a popover open and closed, a button element needs to include an invoketarget attribute with a value that matches the id of the popover.

<button invoketarget="foobar">Toggle popover</button>

<div id="foobar" popover>
  Popover content goes here...
</div>

A <button> with an invoketarget attribute is called an invoker. Invokers might eventually bring all sorts of power to HTML markup, but in its first iteration it’s limited to opening and closing popovers and dialogs. You don’t need onclick= or addEventListener, it’ll just work.

The fact that popovers work without JavaScript is nice, but toggling display: none on an element using JS was never challenging. Popovers do, however, bring far more to the table:

  • Popovers make use of the top layer.
  • Light-dismiss functionality: clicking outside of the popover will close the popover.
  • Hitting the escape key will close the popover.
  • Focus management: when you open a popover, the next tab stop will be the first focusable element inside the popover. If you’ve focused an element within the popover and then close the popover, focus is returned to the correct place (this was tricky to get right with JavaScript).

Browser support

The popover attribute is supported in Chrome, Safari, and Firefox 125. The popovertarget attribute currently has better browser support than invoketarget. popovertarget is popover-specific, offering a declarative way to toggle popovers open and closed. popovertarget will likely eventually be deprecated and replaced by the more flexible invoketarget. After popovers shipped in Chrome, some smart people realised it would also be handy to have a declarative way for buttons to open dialogs and perform other tasks, which is why there are two ways to do the same thing. A polyfill for invokers is available.

Light dismiss

The popover attribute can be set to either auto (the default) or manual. When set to auto, the popover has light dismiss functionality: if the user clicks outside of the popover, the popover is closed. Pressing the escape key will also close the popover. Only one auto popover is ever open at a time.

When set to manual, there is no light dismiss functionality and the escape key does not close the popover. The popover must be explicitly closed by pressing the button again (or by calling hidePopover() in JavaScript). Multiple manual popovers can be open at the same time.

<button invoketarget="foobar">Toggle popover</button>

<div id="foobar" popover="manual">
  Popover content goes here...
</div>

Invoker actions

Along with the invoketarget attribute, a button can also optionally include an invokeaction attribute. The different actions are listed below.

ActionDescription
showpopoverShow a popover.
hidepopoverClose a popover.
showmodalOpen a dialog element as modal.
closeClose a dialog element.

If you omit the invokeaction attribute, the default behaviour depends on the context: If the target set by invoketarget is a popover it will call .togglePopover(). If the target is a dialog it will call showModal() if the dialog is closed and will close the dialog if the dialog is open.

Using invokers for the dialog element looks much the same as the popover example:

<button invoketarget="my-dialog">Open Dialog</button>

<dialog id="my-dialog">
  Dialog content goes here.
  <button invoketarget="my-dialog" invokeaction="close">Close dialog</button>
</dialog>

Along with built-in actions, developers can write custom actions. This is outside the scope of this article as a custom action could do anything — it need not be related to dialogs or popovers.

While a selling point of invokers is forgoing JavaScript, they also provide a new JavaScript invoke event should you need more than the default behaviour. This event is fired on the popover or dialog, not the button.

document.querySelector("[popover]").addEventListener("invoke", function(event) {
    console.log(event.action);
    console.log(event.invoker);
    // do something useful here...
  });

Within the event handler you can get a reference to whichever button triggered the invocation with event.invoker and determine the action specified by invokeaction with event.action.

Popover methods and events

For many use cases, the popover API doesn’t require JavaScript. What if we want to display a toast notification without a user first interacting with a button, for example?

There are methods to show, hide, or toggle a popover element: .showPopover(), .hidePopover() and .togglePopover(), respectively.

document.getElementById('toast').showPopover();

There is a toggle event that fires on the popover both when the popover gets shown and when it gets hidden (there are no separate open or close events). This would be useful for a toast alert that automatically disappears after a set amount of time, for example, as there’s no markup or CSS-based way to do that.

Its worth checking that the popover isn’t already hidden before calling hidePopover(). We can do that with either .matches(':popover-open'), .checkVisibility(), or event.newState === 'open', all of which will return true if the popover is open.

toast.addEventListener("toggle", function (event) {
  if (event.target.matches(":popover-open")) {
    setTimeout(function () {
      toast.hidePopover();
    }, 3000);
  }
});

There’s also a beforetoggle method, which is similar but lets you call event.preventDefault() inside the event handler, should you need to — and it might come in useful for animations. The toggle event, by contrast, isn’t cancellable.

Default popover styles

By default a popover is set to position: fixed and displayed in the center of the viewport with a solid black border but you’re free to style it however you like. The styles the browser applies to a popover look something like this:

[popover] {
    position: fixed;
    width: fit-content;
    height: fit-content;
    inset: 0px;
    margin: auto;
    border: solid;
    padding: 0.25em;
}

If I wanted to position a popover in the bottom left, for example, I’d need to set top and right to either auto, initial or unset.

.toast {
    inset: unset;
    bottom: 12px;
    left: 12px;
}

Beyond z-index: The top layer

Some JavaScript frameworks have something called portals for rendering things like tooltips and dialogs. I always found portals difficult to work with. The React docs describe portals like so:

“Portals let your components render some of their children into a different place in the DOM. This lets a part of your component “escape” from whatever containers it may be in. For example, a component can display a modal dialog or a tooltip that appears above and outside of the rest of the page… You can use a portal to create a modal dialog that floats above the rest of the page, even if the component that summons the dialog is inside a container with overflow: hidden.”

When working with either the <dialog> element (rather than crafting one out of divs) or the popover attribute, you can avoid this issue entirely — no portals required. Their location in the DOM doesn’t matter. Its often convenient to collocate the markup for a popover or <dialog> together with the button that opens it. They can appear anywhere in your markup and won’t get cropped by overflow: hidden on a parent element. They make use of the top layer, which is a native web solution for rendering content above the rest of the document. The top layer sits above the document and always trumps z-index. An element in the top layer can also make use of a styleable ::backdrop pseudo-element.

Animate an element into and out of the top layer

By default, when a popover or dialog is opened, it instantly appears. You might want to add an entry animation — perhaps a quick opacity fade-in, for example. @starting-style is used to animate an element into view with a CSS transition (you don’t need @starting-style when working with @keyframes). @starting-style works both when you’re adding a new element to the DOM and when an element is already in the DOM but is being made visible by changing its display value from display: none. When in a closed state, both the popover attribute and the <dialog> element make use of display: none under the hood, so @starting-style can be used to animate them onto the page.

The following transition will fade and spin the popover into view, and scale down the size of the popover for the exit transition.

/*  Transition to these styles on entry, and from these styles on exit   */
[popover]:popover-open {
  opacity: 1;
  rotate: 0turn;
  transition: rotate .5s, opacity .5s, display .5s allow-discrete, overlay .5s allow-discrete;
}

/*   Entry transition starts with these styles  */
@starting-style {
  [popover]:popover-open {
    opacity: 0;
    rotate: 1turn;
  }
}

/*  Exit transition ends with these styles  */
[popover]:not(:popover-open) {
  scale: 0;
  transition: scale .3s, display .3s allow-discrete, overlay .3s allow-discrete;
}

The popover will transition from its @starting-style styles to its [popover]:popover-open styles every time it’s opened.

The overlay transition is necessary boilerplate when transitioning an element in or out of the top layer. The overlay property was added to CSS purely for this use case and has no other practical application. It is an unusual property to the extent that, outside of transitions, it can only be specified by the browser — you can’t set it with your own CSS. By default, a dialog or popover is instantly removed from the top layer when closed. This will lead to the element getting clipped and obscured. By transitioning overlay, the element stays in the top layer until the transition has finished.

transition-behavior is a new CSS property that can be set to either normal or allow-discrete. In the above code example I’m using the shorthand.

Similarly for the display property, by including it in the transition and specifying transition-behavior: allow-discrete we ensure that a change from display: none happens at the very start of the entrance transition and that a change to display: none happens at the very end of the exit transition.

@starting-style has some useful applications outside of working with popovers and dialogs, but that’s a topic for a different article.

You can transition the ::backdrop pseudo-element in a similar way.

e.g.

@starting-style {
  [popover]:popover-open::backdrop {
    opacity: 0;
  }
}

Now let’s look at doing the same transition with a <dialog> element:

/*  Transition to these styles on entry, and from these styles on exit   */
dialog:open {
  opacity: 1;
  rotate: 0turn;
  transition: rotate .5s, opacity .5s, display .5s allow-discrete, overlay .5s allow-discrete;
}

/*   Entry transition starts with these styles  */
@starting-style {
  dialog:open {
    opacity: 0;
    rotate: 1turn;
  }
}

/*  Exit transition ends with these styles.  */
dialog:closed {
  scale: 0;
  transition: scale .3s, display .3s allow-discrete, overlay .3s allow-discrete;
}

The :open and :closed selectors are new pseudo-selectors. They work for details, dialog, and select elements — but not for popovers. You can use dialog[open] and dialog:not([open]) for the time being for better browser support.

These examples all work in Chrome. @starting-style and transition-behavior are part of Interop 2024, meaning they’ll likely be fully supported by the end of the year. Safari 17.4 added support for transition-behavior: allow-discrete. Safari Technology Preview 189 added support for @starting-style. WebKit have yet to declare a position on the overlay property.

Anchor positioning

With a component like a toast or a dialog, we generally want to position the element in relation to the viewport. We typically display a dialog in the center of the screen, and a toast at the bottom. That’s easy to do. There are other times when you need to position an element in relation to another element on the page. For a dropdown menu, for example, we want to place the popover in relation to the button that opened it. This is more challenging.

Screenshot of the ... three dot menu on YouTube opened up showing a menu of three options: Clip, Save, and Report.

This sort of behaviour usually requires JavaScript and led to the creation of the popular JavaScript libraries Popper, Floating UI and Tether. With the addition of anchor positioning to CSS, we’ll no longer need to reach for JavaScript. The anchor() function allows developers to tether an absolutely positioned element to one or more other elements on the page. Unfortunately, it’s a work-in-progress so I’ll revisit the topic when the spec and implementation are more solid.

Conclusion

I covered a lot in this article but there’s more to come. The popover attribute can be useful all by itself but some forthcoming web APIs will help cover more use cases. Anchor positioning looks set to be the most useful CSS feature since grid. Stay tuned.

]]>
https://frontendmasters.com/blog/menus-toasts-and-more/feed/ 0 1104