Native HTML UI features
Browsers now ship declarative primitives for UI patterns that used to require a JS library. Before pulling in a library or writing custom JS for a modal, popover, accordion, or autocomplete, check if one of these covers it. They're mostly free: less JS shipped, native keyboard/focus handling, native top-layer stacking (no z-index fights).
Support is broad across current evergreen browsers (Chrome/Edge, Firefox, Safari) as of 2026 for all six features below. Still worth a quick caniuse check if the project has a hard requirement on older browsers — these are the newest primitives in this list, so check first: Declarative Shadow DOM and the Popover API.
Decision guide
| Need | Reach for | Not |
|---|---|---|
| Floating menu/tooltip that must sit above everything, dismiss on outside click/ESC | popover attribute |
Popper.js, Floating UI, custom z-index+click-outside JS |
| FAQ/accordion where only one item is open at a time | <details name="group"> |
Accordion component, useState open-index |
| Modal dialog with focus trap, ESC-to-close, backdrop | <dialog> + showModal() |
Custom <div> overlay + focus-trap library |
| Server-rendered Web Component with no FOUC/hydration flash | <template shadowrootmode="open"> |
Client-only attachShadow() |
| Hint the browser which image/script to fetch first (LCP) | fetchpriority="high" |
Manual preload juggling |
| Text input with suggestions but still free-form | <datalist> |
react-select / downshift for simple cases |
1. Popover API
<button popovertarget="my-menu">Toggle Menu</button>
<div id="my-menu" popover>
<p>Native top-layer, light-dismiss on outside click / ESC.</p>
</div>
popovertargeton the trigger wires click-to-toggle automatically — no JS.- The element gets promoted to the browser's top layer, so it renders above everything regardless of CSS
z-index. No stacking-context debugging. popover="manual"disables light-dismiss if you need to control open/close yourself (e.g.el.showPopover()/hidePopover()).- Astro: this is plain HTML behavior, survives View Transitions without any script — no
astro:page-loadwiring needed since there's no JS state to re-init.
2. Accordion groups via <details name="...">
<details name="faq">
<summary>What is modern HTML?</summary>
<p>A living standard evolving directly inside browser engines.</p>
</details>
<details name="faq">
<summary>Do I still need JavaScript?</summary>
<p>Much less of it than you used to.</p>
</details>
Give multiple <details> elements the same name and the browser enforces "only one open at a time" — the classic accordion behavior — with zero JS state. Different name values create independent groups on the same page.
Gotcha: this is exclusive-open only (radio-button semantics). If the design needs multiple sections open simultaneously, just omit name and use plain <details> — don't fight the grouping behavior with JS.
3. Modals via <dialog>
<dialog id="user-modal">
<h2>Confirm Action</h2>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<button
Open Modal
</button>
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
.showModal()(not.show()) is what gives you the modal behavior: top-layer rendering, inert background, native focus trap, and ESC-to-close.<form method="dialog">closes the dialog automatically on submit and setsdialog.returnValueto the clicked button'svalue— no manual close handler needed for the common case..show()(non-modal) does NOT trap focus or disable the background — use.showModal()unless you specifically want a non-modal dialog.- Astro:
showModal()/close()calls live in a regular<script>, notis:inline— see theastro-client-scriptsskill for the scoped-script-over-inline rule. If the trigger button and dialog persist across View Transitions, listen onastro:page-loadto re-attach the open handler after SPA navigation.
4. Declarative Shadow DOM
<user-card>
<template shadowrootmode="open">
<style>
h3 { color: #3b82f6; }
</style>
<h3>Jane Doe</h3>
<p>Senior Frontend Engineer</p>
</template>
</user-card>
The browser parses <template shadowrootmode="open"> and attaches it as a shadow root during HTML parsing — before any JS runs. This is what makes SSR/SSG of Web Components work without a hydration flash (previously attachShadow() had to run client-side, causing FOUC).
Mainly relevant if the project hand-rolls custom elements (customElements.define) rather than Astro/Svelte components — for ordinary Astro component encapsulation, scoped <style> already handles this without touching Shadow DOM at all. Reach for this only when a real Web Component (used outside Astro, e.g. embedded widget) is the actual requirement.
5. fetchpriority
<img src="hero-banner.webp" fetchpriority="high" alt="Product Hero">
<img src="avatar.webp" fetchpriority="low" alt="User avatar">
<script src="app.js" fetchpriority="high"></script>
Explicit hint to the browser's network scheduler about resource importance — complements loading="lazy" and async/defer, doesn't replace them. Use fetchpriority="high" on the actual LCP element (usually the hero image) when the browser's default heuristic guesses wrong, and low on non-critical images competing for bandwidth early in the load. Don't mark everything high — that defeats the point.
6. Native autocomplete via <datalist>
<label for="framework">Choose or type a framework:</label>
<input list="frameworks" id="framework" name="framework" placeholder="e.g. Next.js">
<datalist id="frameworks">
<option value="Next.js"></option>
<option value="SvelteKit"></option>
<option value="Astro"></option>
</datalist>
Zero-JS suggestion list that still allows free-text entry — the browser handles filtering, keyboard nav, and OS-native styling. Good fit for "suggest but don't restrict" inputs. Not a full replacement for combobox components that need custom rendering per option (icons, descriptions, async search) — those still need a real JS component (e.g. <Combobox>-style ARIA pattern).
When NOT to reach for these
- Need custom rendering inside options (icons, multi-line, async-loaded suggestions) →
<datalist>isn't enough, build a proper combobox. - Need multiple dialogs open simultaneously, or non-modal floating panels with complex positioning logic (anchoring to a specific element, flipping on overflow) → Popover API's anchor positioning is more limited than Floating UI; check if
popover+ CSS anchor positioning covers the case before reaching for a library. - Old custom-element codebase already using imperative
attachShadow()client-side → no need to migrate existing working code to declarative Shadow DOM just for its own sake.