Can native web APIs replace custom components in 2025?
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.
What are native web APIs exactly?
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.
The benefits of modern native web APIs
Native web APIs are designed with a few consistent qualities in mind:
- Declarative by default – Many APIs can be implemented with plain HTML. While they expose JavaScript methods and events, they rarely require JavaScript to function. This avoids render-blocking scripts and reduces performance overhead, since the functionality is built directly into the browser engine
- Composable and styleable – These components often come with multiple parts out of the box, like backdrops for
<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 needed - Accessibility baked in – Most APIs are built with accessibility as a first-class concern. While you may still need to adapt them for specific contexts, they remove much of the heavy lifting compared to rolling your own custom component
With those benefits in mind, let’s look at some examples in action.
Dialogs
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:
- Modal dialogs – The main document (
<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) - Non-modal dialogs – The main document remains accessible while the dialog is open
Dialogs are opened with JavaScript:
showModal()
for modal dialogsshow()
for non-modal dialogs
You can close a dialog in a few different ways:
Imperatively (JavaScript):
close()
— non-cancellable, fires theclose
eventrequestClose()
— cancellable, fires thecancel
event
Declaratively (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>
Limitations of <dialog>
- The only way to fire the
cancel
event is through JavaScript’srequestClose()
method; there’s no declarative equivalent - It’s also important not to bypass the official API (for example, by toggling the
open
attribute directly). Doing so breaks built-in behaviors like closing with theEsc
key, CSS hooks (:open
,:modal
,::backdrop
), and accessibility features, all of which are the point of using<dialog>
in the first place - Dialogs aren’t fully declarative compared to newer APIs, since they were developed before this design philosophy took hold. If not for their modal capabilities, they’d almost be considered legacy at this point
Before 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());
See the Pen
<dialog> demo by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
Details disclosures
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:
- Content following
<summary>
is automatically wrapped in a::details-content
pseudo-element. Since 2024, this can be targeted with CSS. It usescontent-visibility: hidden
, which behaves likedisplay: none
but remains searchable via the browser’s “find in page” - The
<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:
See the Pen
<details> demo (accordion) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
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.
Popovers
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-nestedauto
popovershint
– can be light-dismissed, only closeshint
popoversmanual
–can’t be light-dismissed, doesn’t close any other type of popover- Valueless – defaults to
auto
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>
See the Pen
Popover demo by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
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 ofnavigation
<dialog>
has the implicit role ofdialog
(which can be changed toalertdialog
)
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.
Dismissing <dialog>
s
The 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 theesc
key<dialog closedby="any">
– users can close the dialog with a button, theesc
key, or by clicking outside of the dialog
Invoking popups
When 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.
Invoker commands
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>
See the Pen
Invoker Commands API demo (show-modal) by Daniel Schwarz (@mrdanielschwarz)
on CodePen.
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:
- Officially supported
show-modal
(invokesshowModal()
)close
(invokesclose()
)request-close
(invokesrequestClose()
)show-popover
(invokesshowPopover()
)hide-popover
(invokeshidePopover()
)toggle-popover
(invokestogglePopover()
)
- Not supported (oddly)
show
(to invokeshow()
)
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.
Conclusion
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!
The post Can native web APIs replace custom components in 2025? appeared first on LogRocket Blog.
This post first appeared on Read More