diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 1211b96da809a..5b2ef4846bae2 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -226,6 +226,7 @@ mod jsx_a11y {
pub mod html_has_lang;
pub mod iframe_has_title;
pub mod img_redundant_alt;
+ pub mod scope;
}
mod oxc {
@@ -431,5 +432,6 @@ oxc_macros::declare_all_lint_rules! {
jsx_a11y::html_has_lang,
jsx_a11y::iframe_has_title,
jsx_a11y::img_redundant_alt,
+ jsx_a11y::scope,
oxc::no_accumulating_spread
}
diff --git a/crates/oxc_linter/src/rules/jsx_a11y/scope.rs b/crates/oxc_linter/src/rules/jsx_a11y/scope.rs
new file mode 100644
index 0000000000000..140dd450f0316
--- /dev/null
+++ b/crates/oxc_linter/src/rules/jsx_a11y/scope.rs
@@ -0,0 +1,100 @@
+use oxc_ast::{
+ ast::{JSXAttributeItem, JSXElementName},
+ 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, Default, Clone)]
+pub struct Scope;
+
+declare_oxc_lint!(
+ /// ### What it does
+ ///
+ /// The scope prop should be used only on
elements.
+ ///
+ /// ### Why is this bad?
+ /// The scope attribute makes table navigation much easier for screen reader users, provided that it is used correctly.
+ /// Incorrectly used, scope can make table navigation much harder and less efficient.
+ /// A screen reader operates under the assumption that a table has a header and that this header specifies a scope. Because of the way screen readers function, having an accurate header makes viewing a table far more accessible and more efficient for people who use the device.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// // Bad
+ ///
+ ///
+ /// // Good
+ /// | |
+ /// |
+ /// ```
+ Scope,
+ correctness
+);
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("eslint-plugin-jsx-a11y(scope): The scope prop can only be used on elements")]
+#[diagnostic(severity(warning), help("Must use scope prop only on | elements"))]
+struct ScopeDiagnostic(#[label] pub Span);
+
+impl Rule for Scope {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ let AstKind::JSXOpeningElement(jsx_el) = node.kind() else {
+ return;
+ };
+
+ let scope_attribute = match has_jsx_prop_lowercase(jsx_el, "scope") {
+ Some(v) => match v {
+ JSXAttributeItem::Attribute(attr) => attr,
+ JSXAttributeItem::SpreadAttribute(_) => {
+ return;
+ }
+ },
+ None => {
+ return;
+ }
+ };
+
+ let JSXElementName::Identifier(identifier) = &jsx_el.name else {
+ return;
+ };
+
+ let name = identifier.name.as_str();
+ if name == "th" {
+ return;
+ }
+
+ ctx.diagnostic(ScopeDiagnostic(scope_attribute.span));
+ }
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ let pass = vec![
+ (r";", None),
+ (r";", None),
+ (r" | | ", None),
+ (r" | ", None),
+ (r" | ", None),
+ (r" | ", None),
+ // TODO aria-query like parts is needed
+ // (r"", None),
+ // TODO: When polymorphic components are supported
+ // (r"", None)
+ ];
+
+ let fail = vec![
+ (r"", None),
+ // TODO: When polymorphic components are supported
+ // (r";", None),
+ ];
+
+ Tester::new(Scope::NAME, pass, fail).with_jsx_a11y_plugin(true).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/scope.snap b/crates/oxc_linter/src/snapshots/scope.snap
new file mode 100644
index 0000000000000..49467bc6dd80b
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/scope.snap
@@ -0,0 +1,12 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: scope
+---
+ ⚠ eslint-plugin-jsx-a11y(scope): The scope prop can only be used on elements
+ ╭─[scope.tsx:1:1]
+ 1 │
+ · ─────
+ ╰────
+ help: Must use scope prop only on | elements
+
+
|