Skip to main content
    Back to blog
    DevelopmentPublished on October 15, 2026

    Keyboard and Focus: Building a Website You Can Use Without a Mouse

    Focus order, visible focus indicators, skip links, sticky headers, modal dialogs with the native dialog element and composite widgets

    Keyboard and Focus: Building a Website You Can Use Without a Mouse

    Part 4 of 8 in the "Accessible Web in Practice" series. Previous part: Accessible Forms: Labels, Hints, Errors and Autofill.

    A quick test: put the mouse aside and try to complete the main task on your site using the Tab key. Most problems show up within five minutes. This part covers how to make keyboard navigation predictable and comfortable.

    Keyboard and focus

    Focus order

    Focus should move in a logical order — usually the same as the visual one. The order is determined by the DOM, so:

    • don't rearrange elements visually with order, flex-direction: row-reverse or absolute positioning in ways that make the tab order diverge from the reading order;
    • don't use positive tabindex (tabindex="1", "2"...). It breaks the natural order for the entire page.

    tabindex has only two legitimate values:

    Value When
    tabindex="0" Make a custom interactive element focusable (when there's truly no native option)
    tabindex="-1" Make an element focusable programmatically (element.focus()) but not via Tab: a heading after navigation, an error container, a dialog

    Visible focus

    Never do this without a replacement:

    /* ❌ Kills keyboard navigation */
    *:focus { outline: none; }
    

    If the designer dislikes the default outline, replace it using :focus-visible. This pseudo-class applies during keyboard navigation but not on mouse clicks:

    /* ✅ Clear focus for keyboard users only */
    :focus-visible {
      outline: 3px solid #1a5fb4;
      outline-offset: 2px;
    }
    
    /* Remove the outline on mouse click where the browser still draws it */
    :focus:not(:focus-visible) {
      outline: none;
    }
    

    A focus indicator needs contrast against the background (at least 3:1) and enough area to notice. If you draw focus with box-shadow, add a transparent outline — it becomes visible in Windows high contrast mode, where shadows are removed:

    .button:focus-visible {
      box-shadow: 0 0 0 3px #1a5fb4;
      outline: 3px solid transparent; /* shows up in forced-colors mode */
    }
    

    Skip link

    Without one, keyboard users tab through the entire menu on every page.

    <a class="skip-link" href="#main">Skip to main content</a>
    ...
    <main id="main" tabindex="-1">...</main>
    
    .skip-link {
      position: absolute;
      left: 1rem;
      top: -100px;
      padding: 0.5rem 1rem;
      background: #fff;
      color: #000;
      z-index: 1000;
    }
    .skip-link:focus {
      top: 1rem;
    }
    

    The link stays invisible until it receives focus — on the very first Tab.

    Focus must not hide (WCAG 2.4.11)

    The classic case: a sticky header. The user presses Tab, the page scrolls, and the focused element ends up under the header. One line fixes it:

    html {
      scroll-padding-top: 5rem; /* sticky header height + some room */
    }
    

    The same goes for cookie banners and floating chat widgets: they must not cover the focused element.

    Keyboard traps

    Focus must never get stuck in an element with no keyboard way out (criterion 2.1.2). Typical culprits: embedded players, map widgets, third-party iframes, custom editors.

    The only place where focus should stay inside is a modal dialog. And even there, Esc must close it.

    A modal with <dialog>

    An accessible modal used to take dozens of lines: a focus trap, hiding the background from screen readers, Esc handling, returning focus. Today the native <dialog> with showModal() gives you all of that:

    <button type="button" id="open-dialog">Book a consultation</button>
    
    <dialog id="booking" aria-labelledby="booking-title">
      <h2 id="booking-title">Book a consultation</h2>
      <form method="dialog">
        <label for="b-name">Name</label>
        <input id="b-name" name="name" autocomplete="name" required>
    
        <button value="submit">Book</button>
        <button value="cancel" formnovalidate>Cancel</button>
      </form>
    </dialog>
    
    const opener = document.getElementById('open-dialog');
    const dialog = document.getElementById('booking');
    
    opener.addEventListener('click', () => dialog.showModal());
    
    // Modern browsers restore focus on their own, but doing it explicitly is safer
    dialog.addEventListener('close', () => opener.focus());
    

    showModal() makes the rest of the page inert (unfocusable and hidden from screen readers), moves focus inside, closes on Esc, and ::backdrop dims the background. More on MDN: <dialog>.

    For side panels and other cases where part of the page should be temporarily "switched off," there's the inert attribute:

    <main inert>...</main> <!-- unfocusable, hidden from screen readers -->
    

    Composite widgets: arrow keys

    Tabs, menus, listboxes and trees work differently: Tab enters the widget once, and arrow keys move inside it (the "roving tabindex" technique). If you build such a widget yourself, follow the ARIA Authoring Practices Guide — it has a pattern and the expected keyboard model for each one. Better yet, use a proven library (see Accessibility in React: IDs, Focus on Route Change and Announcements).

    The "Accessible Web in Practice" series

    1. Web Accessibility: Who It Really Serves and Where to Start
    2. Semantic HTML and Alt Text: The Foundation of an Accessible Website
    3. Accessible Forms: Labels, Hints, Errors and Autofill
    4. Keyboard and Focus: Building a Website You Can Use Without a Mouse — you are here
    5. Contrast, Motion and Button Size: Visual Accessibility
    6. ARIA and Dynamic Content: When Attributes Help and When They Hurt
    7. Accessibility in React: IDs, Focus on Route Change and Announcements
    8. How to Test Accessibility: Automation, Manual Checks and a Checklist

    Next part: Contrast, Motion and Button Size: Visual Accessibility

    Agama Labs builds websites and web applications. Want to check how accessible your site is? Write to us: hello@agamalabs.com

    Contact

    Let’s talk about your project

    Leave your details and we’ll get back to you within 2 business hours for a free consultation. Tell us what you’re building - we’ll help you choose the right approach.

    Business hours

    Mon–Fri: 08:00 AM – 5:00 PM (PST)
    Sat–Sun: Closed

    Request a call

    Fill out the form and we’ll reach out shortly

    By clicking the button, you agree to our Privacy Policy

    We value your privacy

    We use analytics cookies, including third-party ones, to see how the site is used and improve it