Can native web APIs replace custom components in 2025?

[ad_1]

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

,

, and the Popover API are now widely available.

native apis 2

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 or toggleable content areas for
    . 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

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 () 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 dialogs
  • show() for non-modal dialogs

You can close a dialog in a few different ways:

Imperatively (JavaScript):

  • close() — non-cancellable, fires the close event
  • requestClose() — cancellable, fires the cancel event

Declaratively (HTML):

Limitations of

  • The only way to fire the cancel event is through JavaScript’s requestClose() 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 the Esc key, CSS hooks (:open, :modal, ::backdrop), and accessibility features, all of which are the point of using 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):





  

Dialog content

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());

Details disclosures

Browser support for

disclosures arrived in 2020. By then, new HTML components were designed to be fully declarative, though not fully styleable or animatable until later. A

element enables collapsible, dropdown-like content. Inside it, you must include a

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 , the JavaScript API here is optional and not critical to functionality.

Some key details:

  • Content following
    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”
  • The
    ’s default arrow can be styled or replaced by targeting its ::marker pseudo-element
Summary Content (wrapped in ::details-content)
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

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,

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

for now) the popover attribute with or without a value:

  • auto – can be light-dismissed, closes non-nested auto popovers
  • hint – can be light-dismissed, only closes hint popovers
  • manual –can’t be light-dismissed, doesn’t close any other type of popover
  • Valueless – defaults to auto

In addition, the

Popover content

If a popover contains navigational content, it’s best to use a semantic element like