Skip to content

The tabindex Attribute

Global Attribute

The tabindex attribute controls whether an element can receive keyboard focus and defines its position in the sequential keyboard navigation order (the “tab order”). It’s crucial for keyboard accessibility and focus management in web applications.

<element tabindex="value">Content</element>
ValueDescriptionUse Case
Negative (e.g., -1)Focusable but not in tab orderProgrammatic focus only
0Focusable and in natural tab orderMake custom elements focusable
Positive (e.g., 1, 2)Focusable and in explicit tab orderGenerally avoid
Result
Result
Result
Result

tabindex=“-1” (Programmatic Focus Only)

Section titled “tabindex=“-1” (Programmatic Focus Only)”

Purpose: Make an element focusable via JavaScript, but skip it in tab order.

Use cases:

  • Modal dialogs that need focus when opened
  • Error messages for screen reader announcements
  • Skip link targets
  • Panels in tab interfaces
<!-- Focus moved programmatically -->
<div id="modal" tabindex="-1">
<h2>Modal Title</h2>
<p>Content...</p>
</div>
<script>
document.getElementById('modal').focus();
</script>

Important: Elements with tabindex="-1" can still be clicked with a mouse.

Purpose: Add non-interactive elements to the tab order.

Use cases:

  • Custom interactive components (widgets, custom buttons)
  • Containers that need focus for keyboard interaction
  • Skip navigation links
  • Accessibility landmarks that need focus
<!-- Make a div behave like a button -->
<div tabindex="0" role="button" onclick="handleClick()">
Custom Button
</div>
<!-- Focusable region -->
<div tabindex="0" role="region" aria-label="Interactive map">
<!-- Complex interactive content -->
</div>

Note: Always add tabindex="0" to the correct position in the DOM to maintain logical tab order.

Why to avoid:

  1. Unpredictable order: Disrupts natural document flow
  2. Maintenance nightmare: Hard to track which elements have which values
  3. Accessibility issues: Confuses keyboard users
  4. Breaks expectations: Users expect tab to follow visual order
<!-- DON'T DO THIS -->
<button tabindex="1">First</button>
<input tabindex="3" type="text">
<button tabindex="2">Second</button>
<!-- DO THIS INSTEAD -->
<button>First</button>
<button>Second</button>
<input type="text">

Let the DOM order define tab order:

<!-- Good: Visual and tab order match -->
<form>
<input type="text" name="first">
<input type="text" name="second">
<button>Submit</button>
</form>

Interactive elements are naturally focusable:

<!-- No tabindex needed -->
<button>Click me</button>
<a href="/page">Link</a>
<input type="text">
<select>...</select>
<textarea>...</textarea>

When making custom elements focusable, add appropriate ARIA roles:

<!-- Complete accessible custom button -->
<div
tabindex="0"
role="button"
aria-pressed="false"
onclick="toggle()"
onkeydown="if(event.key==='Enter') toggle()">
Toggle
</div>

Use tabindex="-1" for focus management patterns:

<div id="content" tabindex="-1">
<h1>Page Title</h1>
<!-- Content -->
</div>
<script>
// Skip to content functionality
document.getElementById('skipLink').addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('content').focus();
});
</script>

Always test navigation using only the keyboard:

  • Tab: Move forward through focusable elements
  • Shift+Tab: Move backward
  • Enter: Activate buttons/links
  • Space: Activate buttons, check checkboxes
  • Arrow keys: Navigate within components
<a href="#main" class="skip-link">Skip to main content</a>
<main id="main" tabindex="-1">
<!-- Main content -->
</main>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}
</style>
const modal = document.getElementById('modal');
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
});
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel1" tabindex="0">
Tab 1
</button>
<button role="tab" aria-selected="false" aria-controls="panel2" tabindex="-1">
Tab 2
</button>
<button role="tab" aria-selected="false" aria-controls="panel3" tabindex="-1">
Tab 3
</button>
</div>
<div role="tabpanel" id="panel1" tabindex="0">
Panel 1 content
</div>

Always ensure focused elements are clearly visible:

/* Good focus indicator */
:focus {
outline: 3px solid #3b82f6;
outline-offset: 2px;
}
/* Don't remove outlines without replacement */
:focus {
outline: none; /* Only if you provide an alternative */
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
}
  • tabindex=“0” adds elements to tab order and makes them programmatically focusable
  • tabindex=“-1” makes elements focusable but announces “not in tab order” to some screen readers
  • Always pair with appropriate ARIA roles and labels
<!-- Complete accessible custom element -->
<div
tabindex="0"
role="button"
aria-label="Close dialog"
onclick="close()">
×
</div>

Ensure tab order matches visual layout:

<!-- Bad: Confusing for keyboard users -->
<div style="display: flex; flex-direction: column-reverse;">
<button>Visually Second</button>
<button>Visually First</button>
</div>
<!-- Good: Tab order matches visual order -->
<div style="display: flex; flex-direction: column-reverse;">
<button style="order: 2">Visually Second</button>
<button style="order: 1">Visually First</button>
</div>

Universal support across all modern and legacy browsers:

BrowserSupport
ChromeAll versions
FirefoxAll versions
SafariAll versions
EdgeAll versions
IE5.5+