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). 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>Skip links
Section titled “Skip links”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>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>