diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 699a16a17ad14..3944cf3c0774a 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -231,6 +231,7 @@ mod jsx_a11y {
pub mod img_redundant_alt;
pub mod no_autofocus;
pub mod scope;
+ pub mod tab_index_no_positive;
}
mod oxc {
@@ -439,7 +440,8 @@ oxc_macros::declare_all_lint_rules! {
jsx_a11y::html_has_lang,
jsx_a11y::iframe_has_title,
jsx_a11y::img_redundant_alt,
- jsx_a11y::scope,
jsx_a11y::no_autofocus,
+ jsx_a11y::scope,
+ jsx_a11y::tab_index_no_positive,
oxc::no_accumulating_spread
}
diff --git a/crates/oxc_linter/src/rules/jsx_a11y/tab_index_no_positive.rs b/crates/oxc_linter/src/rules/jsx_a11y/tab_index_no_positive.rs
new file mode 100644
index 0000000000000..815caea35a5bc
--- /dev/null
+++ b/crates/oxc_linter/src/rules/jsx_a11y/tab_index_no_positive.rs
@@ -0,0 +1,118 @@
+use oxc_ast::{
+ ast::{Expression, JSXAttributeItem, JSXAttributeValue, JSXExpression, JSXExpressionContainer},
+ AstKind,
+};
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::Error,
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop_lowercase, AstNode};
+
+#[derive(Debug, Error, Diagnostic)]
+#[error(
+ "eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex."
+)]
+#[diagnostic(severity(warning), help("Change the tabIndex prop to a non-negative value"))]
+struct TabIndexNoPositiveDiagnostic(#[label] pub Span);
+
+#[derive(Debug, Default, Clone)]
+pub struct TabIndexNoPositive;
+
+declare_oxc_lint!(
+ /// ### What it does
+ /// Enforces that positive values for the tabIndex attribute are not used in JSX.
+ ///
+ /// ### Why is this bad?
+ /// Using tabIndex values greater than 0 can make navigation and interaction difficult for keyboard and assistive technology users, disrupting the logical order of content.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// // Bad
+ /// foo
+ ///
+ /// // Good
+ /// foo
+ /// bar
+ /// ```
+ TabIndexNoPositive,
+ correctness
+);
+
+impl Rule for TabIndexNoPositive {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ let AstKind::JSXOpeningElement(jsx_el) = node.kind() else { return };
+ if let Some(tab_index_prop) = has_jsx_prop_lowercase(jsx_el, "tabIndex") {
+ check_and_diagnose(tab_index_prop, ctx);
+ }
+ }
+}
+
+fn check_and_diagnose(attr: &JSXAttributeItem, ctx: &LintContext<'_>) {
+ match attr {
+ JSXAttributeItem::Attribute(attr) => attr.value.as_ref().map_or((), |value| {
+ if let Ok(parsed_value) = parse_jsx_value(value) {
+ if parsed_value > 0.0 {
+ ctx.diagnostic(TabIndexNoPositiveDiagnostic(attr.span));
+ }
+ }
+ }),
+ JSXAttributeItem::SpreadAttribute(_) => {}
+ }
+}
+
+fn parse_jsx_value(value: &JSXAttributeValue) -> Result {
+ match value {
+ JSXAttributeValue::StringLiteral(str) => str.value.parse().or(Err(())),
+ JSXAttributeValue::ExpressionContainer(JSXExpressionContainer {
+ expression: JSXExpression::Expression(expression),
+ ..
+ }) => match expression {
+ Expression::StringLiteral(str) => str.value.parse().or(Err(())),
+ Expression::TemplateLiteral(tmpl) => {
+ tmpl.quasis.get(0).unwrap().value.raw.parse().or(Err(()))
+ }
+ Expression::NumberLiteral(num) => Ok(num.value),
+ _ => Err(()),
+ },
+ _ => Err(()),
+ }
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ let pass = vec![
+ (r";", None),
+ (r"", None),
+ (r#""#, None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r"", None),
+ (r#""#, None),
+ (r#""#, None),
+ (r#""#, None),
+ (r#""#, None),
+ (r#""#, None),
+ (r"", None),
+ (r"", None),
+ ];
+
+ let fail = vec![
+ (r#""#, None),
+ (r"", None),
+ (r#""#, None),
+ (r"", None),
+ (r"", None),
+ ];
+
+ Tester::new(TabIndexNoPositive::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/tab_index_no_positive.snap b/crates/oxc_linter/src/snapshots/tab_index_no_positive.snap
new file mode 100644
index 0000000000000..ee2b148b66b51
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/tab_index_no_positive.snap
@@ -0,0 +1,40 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: tab_index_no_positive
+---
+ ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex.
+ ╭─[tab_index_no_positive.tsx:1:1]
+ 1 │
+ · ────────────
+ ╰────
+ help: Change the tabIndex prop to a non-negative value
+
+ ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex.
+ ╭─[tab_index_no_positive.tsx:1:1]
+ 1 │
+ · ────────────
+ ╰────
+ help: Change the tabIndex prop to a non-negative value
+
+ ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex.
+ ╭─[tab_index_no_positive.tsx:1:1]
+ 1 │
+ · ──────────────
+ ╰────
+ help: Change the tabIndex prop to a non-negative value
+
+ ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex.
+ ╭─[tab_index_no_positive.tsx:1:1]
+ 1 │
+ · ──────────────
+ ╰────
+ help: Change the tabIndex prop to a non-negative value
+
+ ⚠ eslint-plugin-jsx-a11y(tab-index-no-positive): Avoid positive integer values for tabIndex.
+ ╭─[tab_index_no_positive.tsx:1:1]
+ 1 │
+ · ────────────────
+ ╰────
+ help: Change the tabIndex prop to a non-negative value
+
+