oxc-project/oxc · warning
`tabIndex` should only be declared on interactive elements.
Error message
`tabIndex` should only be declared on interactive elements.
What it means
This is the `jsx_a11y/no-noninteractive-tabindex` rule in oxlint. It fires when a `tabIndex` prop (value parsed via `parse_jsx_value`) is declared on a non-interactive element — one that is neither natively interactive (`is_interactive_element`), carrying an interactive role (`is_interactive_role`), nor in the configured custom `tags` list. Adding non-interactive elements to the tab order creates confusing stops with no operable behavior.
Source
Thrown at crates/oxc_linter/src/rules/jsx_a11y/no_noninteractive_tabindex.rs:24
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use oxc_str::CompactStr;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{
AstNode,
context::LintContext,
globals::HTML_TAG,
rule::{DefaultRuleConfig, Rule},
utils::{
get_element_type, has_jsx_prop_ignore_case, is_interactive_element, is_interactive_role,
parse_jsx_value,
},
};
fn no_noninteractive_tabindex_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("`tabIndex` should only be declared on interactive elements.")
.with_help("The `tabIndex` attribute should be removed.")
.with_label(span)
}
#[derive(Debug, Default, Clone, Deserialize)]
pub struct NoNoninteractiveTabindex(Box<NoNoninteractiveTabindexConfig>);
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
struct NoNoninteractiveTabindexConfig {
/// An array of custom HTML elements that should be considered interactive.
tags: Vec<CompactStr>,
/// An array of ARIA roles that should be considered interactive.
roles: Vec<CompactStr>,
/// If `true`, allows tabIndex values to be expression values (e.g., variables, ternaries). If `false`, only string literal values are allowed.
allow_expression_values: bool,
}
View on GitHub (pinned to e1e7af627c)
Solutions
- Replace the element with a native interactive one (`button`, `a href`) so `tabIndex` is unnecessary.
- Remove the `tabIndex` prop if the element is purely presentational.
- If it is genuinely focusable-and-operable via ARIA, add the matching interactive `role` and keyboard handlers.
- For custom elements implemented as interactive, add their tag names to the rule's `tags` config.
Example fix
// before
<div tabIndex={0} onClick={open}>Open card</div>
// after
<button type="button" onClick={open} className="card">Open card</button> Defensive patterns
Strategy: validation
Validate before calling
npx oxlint --jsx-a11y/no-noninteractive-tabindex src/
Type guard
const isInteractiveHost = (props: Record<string, unknown>) => ['button', 'a', 'input', 'select', 'textarea'].includes(String(props.as ?? 'div')) || typeof props.role === 'string' && ['button', 'link', 'checkbox'].includes(props.role);
Prevention
- Only set tabIndex on elements that are also operable via keyboard.
- Register your custom interactive component names in the rule's tags config.
- Prefer tabIndex={0} plus role plus key handlers, or a native element.
When it happens
Trigger: A JSX element with a `tabIndex` attribute whose element type is a non-interactive HTML tag (e.g. `div`, `span`, `li`) or a custom component not listed in the rule's `tags` config, without an interactive role.
Common situations: `<div tabIndex={0}>` used to make cards or rows clickable; focusable spans wrapping icons; UI kit components that forward `tabIndex` to wrappers; teams enabling jsx-a11y after years of div-based interactive components.
Related errors
- Avoid positive integer values for `tabIndex`.
- `{attr_name}` must be accompanied by `onBlur` for accessibil
- ARIA used where native HTML could suffice.
- Missing `alt` attribute.
- Invalid `alt` value.
AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20).
Data as JSON: /api/errors/7cae4c8585fafa65.
Report an issue: GitHub.