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:ring for 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 / linkNative focus and activation. Do not replace with div or span.
Modal dialogFocus moves inside, focus is trapped, Escape closes, focus returns to trigger.
Blocking drawerSame rules as modal when it blocks page interaction.
Dropdown / menuTrigger opens menu, focus moves predictably, Escape closes, focus returns to trigger.
TabsFocus stays in tab list while arrow keys move between tabs; Tab moves to panel content.
TooltipAppears on focus / hover but does not steal focus.
Toast / alertShould not steal focus unless immediate user action is required.
Form validationFocus error summary or first invalid field after submit, not on every keystroke.
Data tableInteractive 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 Tab and Shift+Tab through the component
  • Confirm modal / drawer focus trap works
  • Confirm Escape closes 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.activeElement while 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 tabindex greater than 0
  • Trap focus in dropdowns or non-blocking popovers
  • Move focus unexpectedly after every render
  • Auto-focus destructive actions by default

Developer tips

• Byteion JS handles focus for supported 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.

Unlock Premium Patterns

Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.

Explore Pro Components