Focus System
How Byteion manages focus for keyboard users and screen readers.
Overview
Focus management controls where keyboard focus starts, moves, stays, and returns during interaction. Without it, opening a modal scatters focus to the background, and closing it loses focus entirely — users end up tabbing through invisible elements. Byteion uses focus management especially for overlays and composite widgets. Visible focus indicators are part of the design system, not browser leftovers.
Focus management contract
Interactive Byteion components should follow these rules:
- Show a visible focus state
- Keep focus order logical and close to the visual reading order
- Move focus intentionally when overlays open
- Trap focus only for blocking overlays such as modals and drawers
- Restore focus to the trigger when overlays close
- Avoid focusing hidden, disabled, or inert elements
- Avoid unexpected focus jumps during normal page updates
Core principles
- Visible indicators — Use
focus-visible:ringfor keyboard-only focus rings. Never use* { outline: none }. - Focus trapping — Only for blocking overlays (modals and drawers). Dropdowns, menus, tabs, and popovers should manage focus predictably but should not trap users unnecessarily. Tooltips should not steal focus.
- Focus restoration — When an overlay closes, return focus to the element that opened it.
- Programmatic focus — Use
tabindex="-1"on containers, headings, or error summaries that need.focus(). - Managed focus — Menus, tabs, and roving-tabindex widgets should move focus predictably with arrow keys.
- No hidden focus — Focus should never remain inside hidden or removed DOM.
Pattern-specific focus behaviour
| Pattern | Focus behaviour |
|---|---|
| Button / link | Native focus and activation. Do not replace with div or span. |
| Modal dialog | Focus moves inside, focus is trapped, Escape closes, focus returns to trigger. |
| Blocking drawer | Same rules as modal when it blocks page interaction. |
| Dropdown / menu | Trigger opens menu, focus moves predictably, Escape closes, focus returns to trigger. |
| Tabs | Focus stays in tab list while arrow keys move between tabs; Tab moves to panel content. |
| Tooltip | Appears on focus / hover but does not steal focus. |
| Toast / alert | Should not steal focus unless immediate user action is required. |
| Form validation | Focus error summary or first invalid field after submit, not on every keystroke. |
| Data table | Interactive controls in rows remain reachable in logical order. |
Initial focus rules
- For simple dialogs, focus the dialog title or close button
- For forms, focus the first invalid or first required field when appropriate
- For destructive confirmation dialogs, focus the safest action or the dialog title — not necessarily the destructive button
- For long dialogs, focus the heading or title to give screen reader context
- Avoid auto-focusing fields if it causes mobile keyboard popups unexpectedly
Practical examples
<!-- Focus ring using Byteion semantic tokens -->
<button type="button"
class="inline-flex items-center rounded-button bg-primary px-4 py-2 text-sm font-medium
text-primary-foreground shadow-button
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20">
Save changes
</button>
// Safer focus restoration
let lastTrigger = null;
function openModal(trigger, modal) {
lastTrigger = trigger;
modal.hidden = false;
const target = modal.querySelector(
'[data-autofocus], h2, button, input, select, textarea'
);
target?.focus({ preventScroll: true });
}
function closeModal(modal) {
modal.hidden = true;
if (lastTrigger && document.contains(lastTrigger)) {
lastTrigger.focus({ preventScroll: true });
}
}
Store the trigger before opening. Check that the trigger still exists before restoring focus. Use { preventScroll: true } to avoid unwanted page jumps when helpful.
Focus testing checklist
- Open overlays with keyboard
- Verify initial focus lands in the right place
- Press
TabandShift+Tabthrough the component - Confirm modal / drawer focus trap works
- Confirm
Escapecloses dismissible overlays - Confirm focus returns to the trigger after close
- Confirm focus is visible in light and dark mode
- Confirm hidden content is not focusable
- Inspect
document.activeElementwhile debugging - Test with a screen reader smoke test
- Test real workflows, not only isolated components
Do and Don't
Do
- Use native elements first
- Keep focus visible
- Use
tabindex="-1"for programmatic focus targets - Store and restore the trigger for overlays
- Trap focus only when background interaction is blocked
- Test with keyboard only
Don't
- Use
* { outline: none } - Leave focus on hidden or removed elements
- Use
tabindexgreater than0 - Trap focus in dropdowns or non-blocking popovers
- Move focus unexpectedly after every render
- Auto-focus destructive actions by default
Developer tips
data-bi-* components such as modals and drawers — focus trap, restore, and initial placement.
• Custom compositions still require manual testing. Byteion JS handles built-in patterns, but custom combinations may need additional focus logic.
• Use document.activeElement in DevTools to see which element currently has focus.
• Use :focus-within for wrappers like field groups, but avoid using it as a replacement for direct focus indicators on the interactive element itself.
• Test focus in light and dark mode — focus rings must be visible on all surfaces.Related
- Accessibility Overview — Overall accessibility contract
- Keyboard Navigation — Keyboard interaction patterns
- Modal / Dialog — Focus trap and restore behaviour
- Dropdown / Menu — Managed focus behaviour
- Dark Mode — Focus ring contrast in both themes
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.
Explore Pro Components