Skip to content

Keyboard & Focus

Airframe applies visible focus indicators to all interactive elements automatically. No setup required.

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--af-focus-ring-width) solid var(--af-focus-ring-color);
  outline-offset: var(--af-focus-ring-offset);
}
TokenPurpose
--af-focus-ring-widthRing thickness
--af-focus-ring-colorRing color
--af-focus-ring-offsetGap between element and ring

Override these tokens in your theme to change focus appearance globally.

Native <dialog> with showModal() marks the rest of the page as inert, which prevents tab from leaving the dialog. This covers most use cases but has known gaps:

  • Some browsers (notably Safari) can leak focus outside the dialog
  • Screen reader virtual cursors can navigate past the dialog boundary
  • Focus restoration to the trigger on close is inconsistent across browsers

For production apps, supplement native behavior with a focus trap library (e.g. focus-trap) or a future @airframeui/a11y utility.

<dialog class="af-dialog">
  <div class="af-dialog-header">
    <h2 class="af-dialog-title">
      Title
    </h2>
    <button class="af-btn" onclick="this.closest('dialog').close()">
      Close
    </button>
  </div>
  <div class="af-dialog-body">
    <p>
      Content here.
    </p>
  </div>
</dialog>

Use af-sr-only-focusable for skip links that appear on keyboard focus:

<a href="#main-content" class="af-sr-only-focusable">
  Skip to main content
</a>
<nav class="af-navbar">
  ...
</nav>
<main id="main-content">
  ...
</main>

See Screen Reader Utilities for details.

Never remove focus outlines. Airframe enforces this in base styles.

/* ❌ Never do this */
*:focus { outline: none; }

/_ ✅ Airframe handles focus automatically _/

Use semantic elements. A <button> is keyboard accessible by default. A <div> is not.

<!-- ❌ Not keyboard accessible -->
<div onclick="doSomething()">
  Click me
</div>
<!-- ✅ Keyboard accessible by default -->
<button onclick="doSomething()">
  Click me
</button>

Avoid positive tabindex. It breaks the natural DOM tab order.

<!-- ❌ Breaks tab order -->
<button tabindex="2">
  Second
</button>
<button tabindex="1">
  First
</button>
<!-- ✅ Let DOM order determine tab order -->
<button>
  First
</button>
<button>
  Second
</button>