Most React apps pull in a dependency for modals, tooltips and dropdowns. They handle focus trapping, Escape, click outside, and rendering above everything else. In 2026 the browser does all of that for you. Here is how I used the native pieces when I rewrote my component library, and the few places where you still need some JavaScript.
A modal with <dialog>
"use client"; import { useLayoutEffect, useRef } from "react"; export function Modal({ open, onClose, children }: { open: boolean; onClose: () => void; children: React.ReactNode; }) { const ref = useRef<HTMLDialogElement>(null); useLayoutEffect(() => { const el = ref.current; if (!el) return; if (open && !el.open) el.showModal(); if (!open && el.open) el.close(); }, [open]); return ( <dialog ref={ref} onClose={onClose} onClick={(e) => e.target === e.currentTarget && onClose()} className="rounded-xl p-0 backdrop:bg-black/40" > <div className="p-6">{children}</div> </dialog> ); } showModal() puts the dialog in the top layer, so no portal and no z-index fights. Focus moves inside and returns to the trigger on close. Escape fires the close event, which we forward to onClose. A click on the dialog element itself (not its content) means the backdrop was clicked.
One extra line locks page scroll while any dialog is open:
html:has(dialog[open]) { overflow: hidden; } Animating in and out
dialog { opacity: 0; translate: 0 8px; transition: opacity .2s, translate .2s, overlay .2s allow-discrete, display .2s allow-discrete; } dialog[open] { opacity: 1; translate: 0 0; } @starting-style { dialog[open] { opacity: 0; translate: 0 8px; } } @starting-style defines where the enter animation starts, and allow-discrete keeps the dialog visible until the exit animation finishes. Wrap it in @media (prefers-reduced-motion: no-preference) to respect user settings.
A dropdown with the popover API
<button popoverTarget="menu">Options</button> <div id="menu" popover="auto" role="menu">…</div> popover="auto" closes on Escape and on a click outside, and opening one auto popover closes the others. What the browser does not do yet in every engine is anchor the popover to its button, so I listen for the toggle event and set top/left from getBoundingClientRect(), flipping to the other side when there is no room. Once CSS anchor positioning works in every browser, even that part can go.
Accordion with <details name>
<details name="faq"><summary>Is there a refund?</summary>…</details> <details name="faq"><summary>Can I use it for clients?</summary>…</details> Giving several <details> the same name makes them exclusive: opening one closes the others.
What is left for JavaScript
- Positioning popovers and tooltips.
- Arrow-key navigation inside menus.
- Tooltips that open on hover and focus, which use
popover="manual".
That is roughly a hundred lines, compared with several dependencies.
The result
I used these pieces to rebuild my old react-bootstrap library as Bon UI: 30+ components, React 19 + Tailwind CSS 4, zero runtime dependencies, about 13 KB of JS gzipped. You can browse every component in the Storybook at https://ui.bontemplates.com.
What do you still reach for a library for? I'm curious which native APIs people already trust in production.

