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). Airframe does not ship that JS.

<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-skip. Hidden until focused; then a control at the start of the viewport.

<a href="#main" class="af-skip">Skip to content</a>

<nav class="af-navbar">...</nav>

<main id="main" tabindex="-1">...</main>

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>