Website Spec
Accessibility Recommended Updated 2026-05-29

Native interactive elements

Prefer native HTML interactive elements — <button>, <a>, <details>/<summary>, <dialog> — over divs with click handlers. You get keyboard support, focus management, and assistive-tech semantics for free.

What it is

HTML ships a small set of interactive primitives — <button>, <a href>, <details>/<summary>, <dialog> — that come pre-wired with keyboard handling, focus management, ARIA roles, and browser behaviours. Reach for those first; reach for <div role="button" tabindex="0"> only when the platform genuinely has no element for what you are building. The broader principle of “use the right tag” lives on Semantic HTML; this page is about the interactive ones specifically.

Why it matters

Replacing <div onclick> with <button> is not cosmetic. The native element gives you, with zero code:

  • Keyboard semantics. <button> activates on ENTER and SPACE. <a> activates on ENTER. <dialog> closes on ESC. <details> toggles on ENTER or SPACE when <summary> has focus. Tab order is correct by default.
  • Focus management. <dialog> traps focus inside the modal while open, returns focus to the opener on close, and renders in the top layer above other content.
  • Assistive-tech announcement. Screen readers expose the right role (“button”, “link”, “dialog”, “disclosure”) and state (“expanded”, “modal”) without an aria-* attribute in sight.
  • Browser plumbing. Open <details> state survives back/forward navigation. <a> populates history, supports middle-click and “open in new tab”, honours target and rel.
  • Free maintenance. Every browser ships fixes and refinements (animatable <details>, ::backdrop, ESC-dismiss policy) without you changing a line.

Re-implement any of this in JavaScript and you have signed up to maintain it forever, with bugs.

How to implement

Disclosure / FAQ / accordion<details> and <summary>:

<details>
  <summary>What is the return policy?</summary>
  <p>Returns are accepted within 30 days of delivery.</p>
</details>

Animate with modern CSS — no JavaScript needed:

details::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size 200ms, content-visibility 200ms allow-discrete;
  interpolate-size: allow-keywords;
}
details[open]::details-content { block-size: auto; }

Modal dialog<dialog> with showModal():

<dialog id="confirm">
  <form method="dialog">
    <p>Delete this entry?</p>
    <button value="cancel">Cancel</button>
    <button value="confirm">Delete</button>
  </form>
</dialog>

<button type="button" onclick="confirm.showModal()">Delete</button>

showModal() opens it modally — focus is trapped, ESC dismisses, ::backdrop styles the dim layer. Use show() for non-modal. For transient, non-blocking overlays (menus, tooltips, toasts) use the Popover API instead.

Buttons and links. A <button> does something on the current page; an <a href> goes somewhere. Inside a <form>, always set type="button" on buttons that are not the submit:

<button type="button" onclick="reset()">Reset</button>

Common mistakes

  • <div onclick> as a button. No keyboard activation, no focus, no accessible name, no role.
  • <a href="#" onclick> as a button. Pollutes browser history, breaks middle-click and “open in new tab”, and announces as a link in screen readers.
  • Rebuilding <details> in JavaScript “to control the animation”. Modern CSS — interpolate-size, transition-behavior: allow-discrete, ::details-content — animates the native element.
  • Using <dialog> for transient, non-blocking UI. Reserve <dialog> for modal flows that require a decision. For menus, popovers, and toasts, use the Popover API.
  • Forgetting type="button" inside a <form>. A bare <button> defaults to type="submit" and submits the form on click.

Verification

  • Tab through the page. Every interactive thing must be reachable, in a sensible order, with a visible focus ring.
  • Open Chrome DevTools → Elements → Accessibility tree. Confirm details, dialog, and button are exposed with the correct roles and states.
  • Activate <details>, <button>, and <dialog> triggers using only the keyboard (ENTER, SPACE, ESC).
  • In VoiceOver, open the Web Item Rotor — disclosure widgets and dialogs appear with the right labels.
  • Search the codebase for role="button" and onclick on non-button elements. Each hit is a candidate for replacement.

Related topics

Sources & further reading

Search
esc close navigate open