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

  1. Replace the element with a native interactive one (`button`, `a href`) so `tabIndex` is unnecessary.
  2. Remove the `tabIndex` prop if the element is purely presentational.
  3. If it is genuinely focusable-and-operable via ARIA, add the matching interactive `role` and keyboard handlers.
  4. 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

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


AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20). Data as JSON: /api/errors/7cae4c8585fafa65. Report an issue: GitHub.