Keyboard & Focus
What Airframe provides
Section titled “What Airframe provides”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);
}Focus tokens
Section titled “Focus tokens”| Token | Purpose |
|---|---|
--af-focus-ring-width | Ring thickness |
--af-focus-ring-color | Ring color |
--af-focus-ring-offset | Gap between element and ring |
Override these tokens in your theme to change focus appearance globally.
Focus trapping
Section titled “Focus trapping”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>Skip links
Section titled “Skip links”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.
Rules to follow
Section titled “Rules to follow”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>