Keyboard Navigation with Tailwind CSS: Implementing Skip Links and Focus Order Correctly
AI generated
</>
tw
Tailwind CSS · Accessibility · Keyboard Navigation · WCAG
Keyboard Navigation with Tailwind CSS
implementing skip links and focus order correctly

Anyone not using a mouse relies entirely on working keyboard navigation. Skip links, a logical focus order, and a clean focus trap in modals decide whether a Tailwind CSS application is usable or unusable for keyboard users.

14 min read Skip link · tabindex · focus trap · roving tabindex Tailwind CSS v3 · v4 · WCAG 2.1/2.2 AA

1. Why keyboard navigation is not a niche topic

Motor impairments, visual impairments, temporary injuries, or simply a preferred way of working: the reasons for navigating exclusively with the keyboard are diverse and affect a significantly larger group of users than many teams assume. Working keyboard navigation is also not an additional requirement, but the basic prerequisite on which screen reader use itself is built, since screen readers traverse a page through the same tab order as a pure keyboard user.

Tailwind CSS itself does not restrict keyboard navigation, but utility classes like order-* or flex-row-reverse can subtly change the visual order of elements without changing the underlying DOM order. This exact discrepancy between visual and actual order is one of the most common causes of broken keyboard navigation in modern interfaces built with flexbox and grid.

2. Understanding the native tab order

Without an explicit tabindex, the tab order follows exactly the order of elements in the DOM, top to bottom in the source code. This means the logical structure of an HTML document directly determines a keyboard user's experience, regardless of how the layout is visually arranged later with CSS. This coupling is intentional and one of the reasons why semantic HTML written in a meaningful order remains the foundation of good keyboard navigation.

A common misconception: developers assume the tab order automatically adapts to the visual arrangement once a layout is visually reordered with CSS grid or flexbox. That is wrong, the tab order remains strictly tied to the DOM order. Anyone who visually moves a navigation menu to the right but leaves it in the DOM before the main content still forces keyboard users to tab through the entire navigation first before the main content becomes reachable.

3. Using tabindex correctly: 0, -1, and positive values

The tabindex attribute has three practically relevant values, each triggering completely different behavior. tabindex="0" inserts an element into the natural tab order, at the position it occupies in the DOM, this is the default value for custom components like a <div role="button">. tabindex="-1" removes an element from the tab order, but still allows programmatic focus via JavaScript, for example an error summary that is jumped to via element.focus() but should not be reachable by tabbing.

Positive tabindex values like tabindex="1" or higher are considered a clear antipattern in keyboard navigation. They force their own tab order independent of the DOM order, which quickly leads to a confusing, hard to maintain order once several elements have different positive values. As soon as a new element with a lower positive value is inserted, the entire order shifts unpredictably. The only sustainable solution is to structure the DOM order itself correctly, instead of overriding it with positive tabindex values.


<!-- WRONG: positive tabindex values create a fragile, hard-to-maintain order -->
<button tabindex="3">Submit</button>
<input tabindex="1" type="text" />
<input tabindex="2" type="email" />

<!-- RIGHT: correct DOM order makes tabindex unnecessary -->
<input type="text" name="firstname" />
<input type="email" name="email" />
<button type="submit">Submit</button>

<!-- tabindex="0": makes a custom div focusable, inserted at its DOM position -->
<div role="button" tabindex="0" class="cursor-pointer p-2 bg-sky-600 text-white rounded-lg">
  Custom Action
</div>

<!-- tabindex="-1": focusable only via JavaScript, not part of the Tab order -->
<div id="error-summary" tabindex="-1" role="alert" class="p-4 bg-red-50 rounded-lg">
  Please correct the highlighted fields.
</div>

On every page with repeated navigation, such as a multi level main menu, a keyboard user would have to tab through the entire navigation again on every page load to reach the actual content, unless a skip link exists. A skip link is the very first focusable link on the page and jumps directly to the main content when activated, usually implemented via an anchor to id="main-content".

With Tailwind CSS, this link can be kept invisible via sr-only focus:not-sr-only until it is focused with the Tab key, then it appears visibly at the top of the screen. This combination is an established keyboard navigation pattern and should exist on every page with more than a short navigation menu, especially for multilingual shops with long category menus.


<!-- Skip link as the very first focusable element on the page -->
<body>
  <a
    href="#main-content"
    class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4
           focus:z-50 focus:bg-white focus:text-sky-700 focus:px-4 focus:py-2
           focus:rounded-lg focus:shadow-lg"
  >
    Skip to main content
  </a>

  <header>
    <nav aria-label="Main navigation"><!-- long navigation menu --></nav>
  </header>

  <main id="main-content" tabindex="-1">
    <!-- page content the skip link jumps to -->
  </main>
</body>

5. Focus order pitfalls with CSS reordering

The Tailwind utility order-* and the flex direction variant flex-row-reverse exclusively change the visual arrangement of elements, never the underlying focus order. A card with an image visually moved to the top via order-first, even though it sits after the heading in the DOM, leads to a confusing experience: sighted users see the image first, but a keyboard user focuses the heading first, because the DOM order remains unchanged.

This discrepancy is explicitly addressed by WCAG success criterion 1.3.2 "Meaningful Sequence": the order in which content is perceived must remain meaningful regardless of the presentation technique. The most reliable way to avoid this problem is to use visual reordering via CSS sparingly and, when in doubt, adjust the DOM order directly to match the intended visual and logical order instead of correcting it after the fact with order.


<!-- WRONG: order-first visually moves the image up, but DOM order (and
     therefore focus/reading order) still starts with the heading -->
<div class="flex flex-col">
  <h3 class="order-2">Product name</h3>
  <img class="order-1" src="/product.jpg" alt="Product photo" />
  <button class="order-3" tabindex="0">Add to cart</button>
</div>

<!-- RIGHT: DOM order matches the intended visual and focus order directly -->
<div class="flex flex-col">
  <img src="/product.jpg" alt="Product photo" />
  <h3>Product name</h3>
  <button type="button">Add to cart</button>
</div>

6. Modals and dialogs: trapping focus

As soon as a modal dialog is opened, keyboard focus must stay trapped inside the dialog, a so called focus trap. Without this mechanism a user accidentally tabs out of the visible modal, directly into the underlying, actually inaccessible page, while the dialog optically still appears in the foreground. This discrepancy between visual modality and actual focus is one of the most common accessibility mistakes in self built dialog components.

A correct focus trap sets focus to the first focusable element in the dialog on open, intercepts Tab and Shift Tab at the last and first focusable elements respectively and jumps to the other end, and restores focus on close to the element that originally opened the dialog. Alpine.js offers a ready made implementation of this pattern with the x-trap plugin, which can be used directly in Hyvä projects instead of implementing a focus trap from scratch.


// Alpine.js x-trap: traps focus inside the modal while it is open
document.addEventListener('alpine:init', () => {
  Alpine.data('productModal', () => ({
    open: false,
    triggerElement: null,

    openModal() {
      // remember what triggered the modal, to restore focus on close
      this.triggerElement = document.activeElement;
      this.open = true;
    },

    closeModal() {
      this.open = false;
      // restore focus to the element that originally opened the modal
      this.triggerElement?.focus();
    },
  }));
});

A dropdown menu that only opens and closes via mouse click, but does not process keyboard events, remains unreachable for keyboard users, even if the triggering button itself is focusable. A fully accessible dropdown responds at minimum to Enter and space to open, to arrow keys for navigating between menu items, and to Escape to close with focus returning afterward to the triggering button.

Arrow key navigation within an open dropdown does not follow the normal tab order, but its own navigation pattern implemented in JavaScript, since ARIA menus count as standalone widgets with their own keyboard semantics. Escape must work reliably and return focus to the triggering button, otherwise a keyboard user completely loses orientation on the page after closing.


// Alpine.js dropdown: arrow key navigation plus Escape to close and refocus
function dropdown() {
  return {
    open: false,
    activeIndex: 0,
    items: [],

    init() {
      this.items = Array.from(this.$refs.menu.querySelectorAll('[role="menuitem"]'));
    },

    onKeydown(event) {
      if (event.key === 'ArrowDown') {
        event.preventDefault();
        this.activeIndex = (this.activeIndex + 1) % this.items.length;
        this.items[this.activeIndex].focus();
      }
      if (event.key === 'ArrowUp') {
        event.preventDefault();
        this.activeIndex = (this.activeIndex - 1 + this.items.length) % this.items.length;
        this.items[this.activeIndex].focus();
      }
      if (event.key === 'Escape') {
        this.open = false;
        this.$refs.trigger.focus();
      }
    },
  };
}

8. Roving tabindex for complex widgets

With complex widgets like tabs, toolbars, or a grid of several buttons, it usually does not make sense to include every single element in the normal tab order. A user who has to tab through ten toolbar buttons one by one to reach the next section of the page perceives this as unnecessarily slow. The roving tabindex pattern solves this problem: only a single element of the group has tabindex="0" at any given time, all others have tabindex="-1", navigation within the group happens via arrow keys instead of Tab.

When switching the active element within the group, for example by pressing the right arrow key in a tab bar, the tabindex of the previously active element is set to -1 and that of the new active element to 0. A single Tab keypress then leaves the entire group in one step, instead of traversing every single element separately. This pattern follows exactly the WAI ARIA authoring practices for tab and toolbar widgets and is the standard approach for performant keyboard navigation in widget heavy interfaces.

9. Keyboard navigation patterns compared

The following overview shows common implementation mistakes in keyboard navigation and the respective recommended, robust alternative.

Area Common mistake Recommended pattern Benefit
Controlling order Positive tabindex values Correct DOM order Maintainable, no unexpected jumps
Long navigation No skip link present sr-only skip link Direct jump to main content
Visual reordering order-* only changes visuals Adjust DOM order Focus follows visual logic
Modal dialog Focus leaves the dialog Focus trap with restore Focus stays controlled
Toolbar with many buttons Each button tabbable individually Roving tabindex One tab stop for the whole group

This table makes clear that good keyboard navigation usually requires no additional technology, but a conscious decision to use native browser mechanisms like DOM order and tabindex correctly, instead of unintentionally bypassing them.

Mironsoft

Tailwind CSS, accessibility and WCAG-compliant frontend development

Fully operable by keyboard?

We audit existing interfaces for missing skip links, broken focus order, and unsecured modals, and implement focus trap as well as roving tabindex for complex widgets, so your application stays fully operable by keyboard.

Keyboard Audit

Complete manual review of tab order across all pages

Focus Trap Integration

Retrofit modals and dialogs with correct focus management

Widget Patterns

Roving tabindex for tabs, toolbars, and complex components

10. Summary

Working keyboard navigation is based on DOM order, not visual arrangement, which is why Tailwind utilities like order-* must be used with caution. tabindex="0" makes custom components focusable, tabindex="-1" allows programmatic focus without a tab stop, positive values are an antipattern to avoid. Skip links with sr-only focus:not-sr-only save keyboard users from repeatedly tabbing through long navigation menus.

Modals need a focus trap that sets focus on open, keeps it trapped while open, and restores it to the trigger on close. Dropdown menus must process arrow keys and Escape, complex widgets like tabs and toolbars benefit from roving tabindex, which includes only one element of the group in the normal tab order. Together these patterns produce an application that stays fully usable without a mouse.

Keyboard Navigation with Tailwind CSS - the essentials at a glance

DOM order determines focus

Visual CSS reordering like order-* does not change the tab order.

Skip links on every page

sr-only focus:not-sr-only saves keyboard users from tabbing through long navigation.

Focus trap in modals

Set focus on open, trap it while displayed, restore it on close.

Roving tabindex for widgets

Only one element of the group in the tab order, navigation within via arrow keys.

11. FAQ: Keyboard Navigation with Tailwind CSS

1What determines the tab order?
The order of elements in the DOM, independent of the visual CSS arrangement.
2Why are positive tabindex values problematic?
They create a fragile order independent of the DOM, correct DOM order is more sustainable.
3What is a skip link?
The first focusable link, allows bypassing repeated navigation to the main content.
4Does order-* change the focus order?
No, only visual. Focus still follows DOM order, which can lead to discrepancies.
5What is a focus trap?
Keeps focus inside an open dialog, prevents accidentally tabbing into the background page.
6Where does focus go on close?
Back to the element that opened the dialog, so orientation is preserved.
7What keys does a dropdown need?
Enter/space to open, arrow keys for navigation, Escape to close with focus restore.
8What is roving tabindex?
Only one element of the group has tabindex 0, navigation within via arrow keys instead of Tab.
9How do you test manually?
Put the mouse away, navigate only with the keyboard, watch for logical order and visible focus.
10Does Alpine.js help with a focus trap?
Yes, the x-trap plugin provides a ready made implementation for Hyvä projects.