The web is evolving at an incredible pace. I’ve been writing about web development for over a decade (and building websites even longer), but for the first time, it feels challenging to keep up. While we may never see “HTML6” or“CSS4,” new standards continue to emerge and browsers are adopting them faster than ever. Features like <dialog>
, <details>
, and the Popover API are now widely available.
With accessibility, declarative (HTML-first) code, and flexible CSS capabilities at the forefront, the question arises – Do we still need custom components?
This isn’t a “native vs. framework” debate. Frameworks can and do use these APIs, but one of their core selling points “it just works” feels less relevant now that browsers are delivering native APIs that also just work. These features are simple to implement, performant, and often accessible out of the box. And personally, they’ve brought me more joy in web development than anything in years.
In this article, we’ll look at modern native web APIs and how you can use them to build powerful, accessible functionality without extra dependencies or performance overhead.
Broadly speaking, when I say native web APIs, I’m referring to modern HTML, CSS, and JavaScript features that handle tasks we once needed frameworks or complex engineering for. More specifically, “API” here means a set of web features across HTML, CSS, and JavaScript designed to work together to form a fully functional component, like a modal. These aren’t always drop-in components that just work out of the box, but they provide the building blocks to assemble them quickly and effectively.
Native web APIs are designed with a few consistent qualities in mind:
<dialog>
or toggleable content areas for <details>
. Crucially, they aren’t locked down; CSS gives us pseudo-elements, pseudo-classes, and properties to style and customize them as neededWith those benefits in mind, let’s look at some examples in action.
The <dialog>
element shipped in 2013, right at the start of this shift toward more native web features.
Dialogs are a type of popup and can be either modal or non-modal:
<body>
) becomes inert using the inert attribute, trapping focus inside the dialog until it’s closed. The rest of the page is obscured by a customizable ::backdrop
pseudo-element (introduced in 2022)Dialogs are opened with JavaScript:
showModal()
for modal dialogsshow()
for non-modal dialogsYou can close a dialog in a few different ways:
Imperatively (JavaScript):
close()
— non-cancellable, fires the close
eventrequestClose()
— cancellable, fires the cancel
eventDeclaratively (HTML):
<dialog><form><button formmethod="dialog">…</button></form></dialog>
<dialog><form><input type="submit" formmethod="dialog"></form></dialog>
<dialog><form><input type="image" formmethod="dialog"></form></dialog>
<dialog><form method="dialog"><!-- <button|input> here --></form></dialog>
<dialog>
cancel
event is through JavaScript’s requestClose()
method; there’s no declarative equivalentopen
attribute directly). Doing so breaks built-in behaviors like closing with the Esc
key, CSS hooks (:open
, :modal
, ::backdrop
), and accessibility features, all of which are the point of using <dialog>
in the first placeBefore moving on, here’s a complete example of a button-controlled dialog (see MDN for more details):
<button id="show-non-modal-dialog">Show non-modal dialog</button> <button id="show-modal-dialog">Show modal dialog</button> <dialog id="non-modal-dialog"> <div>Dialog content</div> <!-- Close-dialog button --> <form method="dialog"> <button>Close dialog</button> </form> </dialog> <dialog id="modal-dialog"> <!-- An actual form --> <form method="post"> <div>Form content</div> <!-- Submit-form button --> <input type="submit" value="Submit form"> <!-- Close-dialog button --> <button formmethod="dialog">Close dialog</button> </form> </dialog>
dialog { &:open { /* Styles for dialogs that are open */ } &:modal { /* Styles for modal dialogs that are open */ } &:not(:modal) { /* Styles for non-modal dialogs that are open */ } &::backdrop { /* Styles for backdrops */ } } body { &:has(dialog:open) { /* Styles for if a dialog is open */ } &:has(dialog:modal) { /* Styles for if a modal dialog is open */ } &:has(dialog:not(:modal)) { /* Styles for if a non-modal dialog is open */ } &[inert] { /* Styles for if is inert for *any* reason */ } }
const showNonModalDialogButton = document.querySelector("#show-non-modal-dialog"); const showModalDialogButton = document.querySelector("#show-modal-dialog"); const nonModalDialog = document.querySelector("#non-modal-dialog"); const modalDialog = document.querySelector("#modal-dialog"); /* Show dialog non-modally */ showNonModalDialogButton.addEventListener("click", () => nonModalDialog.show()); /* Show dialog modally */ showModalDialogButton.addEventListener("click", () => modalDialog.showModal()); /* Close dialog (we can do this declaratively) */ // closeDialogButton.addEventListener("click", () => dialog.close()); /* Close dialog (cancellable, can't be done declaratively) */ // closeDialogButton.addEventListener("click", () => dialog.requestClose());
Browser support for <details>
disclosures arrived in 2020. By then, new HTML components were designed to be fully declarative, though not fully styleable or animatable until later. A <details>
element enables collapsible, dropdown-like content. Inside it, you must include a <summary>
element, which acts as the toggle button. When clicked, it reveals the associated content. Accessibility is largely handled by default, though it’s still worth reviewing the documentation to avoid mistakes. Unlike <dialog>
, the JavaScript API here is optional and not critical to functionality.
Some key details:
<summary>
is automatically wrapped in a ::details-content
pseudo-element. Since 2024, this can be targeted with CSS. It uses content-visibility: hidden
, which behaves like display: none
but remains searchable via the browser’s “find in page”<summary>
’s default arrow can be styled or replaced by targeting its ::marker
pseudo-element<details> <summary>Summary</summary> Content (wrapped in ::details-content) </details>
details { /* Toggle button */ summary { /* Up/down arrow */ &::marker { } } /* Content to be toggled */ &::details-content { } /* details when open */ &:open { summary { &::marker { /* Replace default arrow */ content: "Down arrow, or something"; } } &::details-content { } } /* details when not open */ &:not(:open) { summary { &::marker { } } &::details-content { } } }
2024 also brought us the ability to have exclusive accordions where only one <details>
in a defined set can be open
at a time. To make that definition, give them the name
attribute with matching values, just as you’d define a set of exclusive radio inputs:
Once again, there are some nitty-gritty details to be aware of (pun not intended), but those aside, <details>
gives us a lot of functionality in exchange for writing very little HTML and CSS.
The Popover API is used to overlay content. In terms of accessibility, it describes the content and behavior of the elements that display the content, but not the nature of the component, since a popover can be many things (a tooltip, a dropdown, or even a non-modal dialog).
In terms of syntax, just give the component (we’ll just use a generic <div>
for now) the popover
attribute with or without a value:
auto
– can be light-dismissed, closes non-nested auto
popovershint
– can be light-dismissed, only closes hint
popoversmanual
–can’t be light-dismissed, doesn’t close any other type of popoverauto
In addition, the <button>
that triggers the popover must reference the id
of the component using the popovertarget
attribute, and you can also throw in popovertargetaction
with either the hide
, show
, or toggle
value if you want to restrict to a specific action (toggle
is the default value).
Example:
<button popovertarget="popover" popovertargetaction="show">Show popover</button> <button popovertarget="popover" popovertargetaction="toggle">Toggle popover</button> <div id="popover" popover="auto"> <div>Popover content</div> <button popovertarget="popover" popovertargetaction="hide">Hide popover</button> </div>
If a popover contains navigational content, it’s best to use a semantic element like <nav>
instead of a generic <div>
. The Popover API can handle non-modal dialog–like behavior, but semantics still matter for accessibility:
<nav>
has the implicit ARIA role of navigation
<dialog>
has the implicit role of dialog
(which can be changed to alertdialog
)Choosing the correct element ensures the right accessibility hints are baked in. If no semantic element fits, you can fall back to <div>
with an explicit role
attribute.
On the styling side, the :popover-open
pseudo-class lets you target open popovers directly in CSS (or use :not(:popover-open)
for closed states):
[popover]:popover-open { /* Styles for popovers that are open */ }
And, of course, there’s a JavaScript API too should we need it. To learn more about popovers, I recommend reading MDN’s Popover API documentation.
<dialog>
sThe three popover types (auto
, hint
, manual
) now have a parallel in dialogs, thanks to the closedby
attribute. This makes it easier to use the more semantic <dialog>
element instead of a popover with role="dialog"
, while also giving you more control over how dialogs are dismissed:
<dialog closedby="none">
– users can’t close the dialog<dialog closedby="closerequest">
– users can close the dialog with a button or the esc
key<dialog closedby="any">
– users can close the dialog with a button, the esc
key, or by clicking outside of the dialogWhen a HTML target (e.g., <button>
) invokes a popup (e.g., dialog or popover) whose role implicitly or explicitly resolves to menu
, listbox
, tree
, grid
, or dialog
, you must include the aria-haspopup
attribute with the same value. For dialogs, this is because there isn’t an attribute that hints at what happens. Although popovers do have such attributes(e.g., popovertargetaction
), they don’t describe what type of popover will pop up, which is exactly what aria-haspopup
does.
This doesn’t apply to <details>
disclosures because the <summary>
button is nested within the declarative component and is semantic.
Currently, the only exception to this rule is the upcoming Interest Invoker API (which is to be combined with popover to create hover-triggered popovers), which includes a ‘minimum’ ARIA role of tooltip
.
Despite all of this, these HTML-first components lessen the accessibility work that we need to do by a considerable amount.
The Invoker Commands API can be used to invoke the JavaScript methods of other native web APIs using just HTML. As an example, it enables us to show a dialog using HTML rather than the show()
or showModal()
JavaScript method, essentially putting the Dialog API on-par with newer native web APIs that were built to be declarative from day one. Releasing just this year (2025), it suggests that the web will be markup-first moving forward.
To use the Invoker Commands API, start off by identifying the target (e.g., <dialog id="modal-dialog">
). After that, create the <button>
that will invoke the target (it specifically must be a button). Finally, add the command
attribute specifying which command to invoke and the commandfor
attribute referencing the id
value (e.g., <button command="show-modal" commandfor="modal-dialog">Show modal</button>
).
Example:
<!-- Equivalent to showModal() --> <button command="show-modal" commandfor="modal-dialog">Show modal</button> <dialog id="modal-dialog"> <div>Dialog content</div> <!-- Equivalent to close() --> <button command="close" commandfor="modal-dialog">Close dialog</button> </dialog>
Official documentation says that the Invoker Commands API can be used to show/close modals and show/hide/toggle popovers, but Open UI states that the API will include other commands eventually. That being said, when I covered the API back in November 2024, many commands (such as open
/close
/toggle
for the <details>
element) had been implemented secretly. Here’s the list of candidates for future support (although it’s possible that more of these have been secretly implemented by now), and below, what’s officially supported:
show-modal
(invokes showModal()
)close
(invokes close()
)request-close
(invokes requestClose()
)show-popover
(invokes showPopover()
)hide-popover
(invokes hidePopover()
)toggle-popover
(invokes togglePopover()
)show
(to invoke show()
)Accessibility depends on the methods that you’re invoking, but is mostly baked in, and if you need a JavaScript API(which you can use to implement custom commands), that has your back too.
We’ll stop here for today, but it’s clear that native web APIs have come a long way. While browser support still has gaps, the trajectory is promising. Soon, we’ll likely see broader adoption of additional invoker commands, the Interest Invoker API (enabling hover-triggered popovers), and proposals for native comboboxes, menus, and more.
There’s even a proposal to extend <input type="checkbox">
into a native switch/toggle, similar to the upgraded <select>
elements shipped earlier this year. These upgrades would let developers target component parts directly with CSS, eliminating the need to hack together custom versions from scratch.
All of this points to a healthier web ecosystem: fewer build and dependency errors, fewer custom bugs, better accessibility, cleaner code, and a reduced reliance on JavaScript. The result is a faster, more reliable, and more joyful developer experience.
If you’ve made it this far, you now know which native web APIs to start experimenting with and what exciting developments are just around the corner.
Thanks for reading, and until next time!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowRead about how the growth of frontend development created so many tools, and how to manage tool overload within your team.
Discover what you actually need to build and ship AI-powered apps in 2025, with tips for which tools to choose and how to implement them.
Compare the top AI development tools and models of September 2025. View updated rankings, feature breakdowns, and find the best fit for you.
Explore the new mode that introduced file-based routing in v7, why it remains optional, and when to use it or stick with a different approach.