Skip to content

The HTML elements nobody uses

Every year we npm install something the browser already has. Six elements and one attribute that replace a library each, with the caveats that stop people finding them.

3 min read
Contents

I reviewed a project last month with a 40 KB dropdown library, a modal library, and a tooltip library. All three had accessibility bugs. All three had a native equivalent shipping in every browser the client supports.

This is not a scolding — I have done it too, because these elements were bad for years and we all learned to avoid them. They are not bad any more. Here is what changed.

<dialog>

Modals are the classic example of something that looks easy and is not. Focus trapping, restoring focus on close, escape to dismiss, inert background, scroll locking, the backdrop.

<dialog id="confirm">
  <form method="dialog">
    <h2>Delete this form?</h2>
    <p>Submissions will be kept for 30 days.</p>
    <button value="cancel">Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>
confirm.showModal();
confirm.addEventListener('close', () => {
  if (confirm.returnValue === 'delete') destroy();
});

showModal() gives you the focus trap, escape handling, inert background and ::backdrop for free. method="dialog" closes the dialog on submit and puts the button’s value in returnValue, which is a neat little API almost nobody knows about.

The one thing you still do yourself is prevent the page behind from scrolling — and even that has been improving. Style the backdrop with dialog::backdrop, and remember showModal() (modal, with backdrop) is not show() (non-modal, no backdrop).

popover

An attribute, not an element, and it covers the entire category of “thing that appears above other things and closes when you click away”: menus, tooltips, notification panels, custom selects.

<button popovertarget="menu">Options</button>

<div id="menu" popover>
  <a href="/settings">Settings</a>
  <a href="/logout">Log out</a>
</div>

No JavaScript at all. The browser gives you the top layer, light dismiss (click outside or press escape), and correct focus behaviour. popover="manual" if you want to control dismissal yourself.

Pair it with CSS anchor positioning — where support allows — and the last reason to install a floating-element library goes away:

#menu {
  position-anchor: --opts;
  position-area: block-end span-inline-start;
}

Anchor positioning is newer than popover and not yet everywhere, so treat the positioning as the enhancement and make sure the popover is usable without it.

<details> and <details name>

Accordions have been <details> for years. What is newer, and what most people missed, is the name attribute:

<details name="faq" open>
  <summary>How do I connect a form?</summary>
  <p>…</p>
</details>
<details name="faq">
  <summary>Can I export submissions?</summary>
  <p>…</p>
</details>

Same name, and opening one closes the others — an exclusive accordion, with zero JavaScript and correct keyboard behaviour. That is a component I have hand-written at least a dozen times.

You can also style the marker (summary::marker or list-style: none plus your own icon), and <details> content is findable by the browser’s in-page search even while collapsed, which no JavaScript accordion manages.

<datalist>

Autocomplete suggestions on a text input, without giving up free text:

<input list="countries" name="country" />
<datalist id="countries">
  <option value="United Arab Emirates"></option>
  <option value="Pakistan"></option>
  <option value="Portugal"></option>
</datalist>

The trade-off is real: you cannot style the dropdown. If the design demands custom rows with avatars, you need a component. But for the very common case of “here are some suggestions, or type your own”, this is one element and it works with the platform’s own filtering.

<output>

For a calculated value that should be announced when it changes. It is an implicit live region, which is the whole reason to use it instead of a <span>:

<form oninput="total.value = (qty.valueAsNumber * 49).toFixed(2)">
  <input type="number" name="qty" id="qty" value="1" />
  <output name="total" for="qty">49.00</output>
</form>

A screen reader user hears the total update. With a <span> they do not, unless you remembered aria-live, and in my experience nobody remembers aria-live.

<progress> and <meter>

Two elements for two different things, and they get mixed up constantly.

<progress> is for a task that completes — an upload, an import. Omit the value attribute and it renders as indeterminate, which is exactly the loading spinner case.

<meter> is for a measurement within a known range that is not going anywhere — disk usage, a score, how full a quota is. It takes low, high and optimum, and browsers colour it accordingly.

<meter value="0.82" low="0.5" high="0.9" optimum="0.2">82% of quota</meter>

Both are announced correctly by assistive technology, which a styled <div> with a width percentage is not.

Why this matters more than it looks

Every one of these ships less JavaScript, which is the cheapest performance win available. But the accessibility argument is the stronger one.

Native elements come with keyboard handling, focus management and the right roles, maintained by browser engineers, tested against real assistive technology, and improved without you doing anything. The library version has whatever its author got round to, frozen at the version in your lockfile.

Not every case fits — sometimes the design genuinely requires a custom component, and then you build one properly. But the default should be the element, and the library should be the exception you can justify. For most teams, that is the reverse of the current situation.

Share