diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 34d8df57117d..429a4c905599 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -243,6 +243,7 @@ mod jsx_a11y {
pub mod no_aria_hidden_on_focusable;
pub mod no_autofocus;
pub mod no_distracting_elements;
+ pub mod prefer_tag_over_role;
pub mod scope;
pub mod tab_index_no_positive;
}
@@ -469,6 +470,7 @@ oxc_macros::declare_all_lint_rules! {
jsx_a11y::no_access_key,
jsx_a11y::no_aria_hidden_on_focusable,
jsx_a11y::no_autofocus,
+ jsx_a11y::prefer_tag_over_role,
jsx_a11y::scope,
jsx_a11y::tab_index_no_positive,
jsx_a11y::no_distracting_elements,
diff --git a/crates/oxc_linter/src/rules/jsx_a11y/prefer_tag_over_role.rs b/crates/oxc_linter/src/rules/jsx_a11y/prefer_tag_over_role.rs
new file mode 100644
index 000000000000..08d11a5c1aec
--- /dev/null
+++ b/crates/oxc_linter/src/rules/jsx_a11y/prefer_tag_over_role.rs
@@ -0,0 +1,133 @@
+use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop_lowercase, AstNode};
+use once_cell::sync::Lazy;
+use oxc_ast::{
+ ast::{JSXAttributeItem, JSXAttributeValue, JSXElementName},
+ AstKind,
+};
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::{self, Error},
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+use phf::phf_map;
+
+#[derive(Debug, Error, Diagnostic)]
+#[error(
+ "eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `{tag}` over `role` attribute `{role}`."
+)]
+#[diagnostic(
+ severity(warning),
+ help("Replace HTML elements with `role` attribute `{role}` to corresponding semantic HTML tag `{tag}`.")
+)]
+struct PreferTagOverRoleDiagnostic {
+ #[label]
+ pub span: Span,
+ pub tag: String,
+ pub role: String,
+}
+#[derive(Debug, Default, Clone)]
+pub struct PreferTagOverRole;
+
+declare_oxc_lint!(
+ /// ### What it does
+ /// Enforces using semantic HTML tags over `role` attribute.
+ ///
+ /// ### Why is this bad?
+ /// Using semantic HTML tags can improve accessibility and readability of the code.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// // Bad
+ ///
+ ///
+ /// // Good
+ ///
+ /// ```
+ PreferTagOverRole,
+ correctness
+);
+
+impl PreferTagOverRole {
+ fn check_roles<'a>(
+ role_prop: &JSXAttributeItem<'a>,
+ role_to_tag: &phf::Map<&str, &str>,
+ jsx_name: &JSXElementName<'a>,
+ ctx: &LintContext<'a>,
+ ) {
+ if let JSXAttributeItem::Attribute(attr) = role_prop {
+ if let Some(JSXAttributeValue::StringLiteral(role_values)) = &attr.value {
+ let roles = role_values.value.split_whitespace();
+ for role in roles {
+ Self::check_role(role, role_to_tag, jsx_name, attr.span, ctx);
+ }
+ }
+ }
+ }
+
+ fn check_role<'a>(
+ role: &str,
+ role_to_tag: &phf::Map<&str, &str>,
+ jsx_name: &JSXElementName<'a>,
+ span: Span,
+ ctx: &LintContext<'a>,
+ ) {
+ if let Some(tag) = role_to_tag.get(role) {
+ match jsx_name {
+ JSXElementName::Identifier(id) if id.name != *tag => {
+ ctx.diagnostic(PreferTagOverRoleDiagnostic {
+ span,
+ tag: (*tag).to_string(),
+ role: role.to_string(),
+ });
+ }
+ _ => {}
+ }
+ }
+ }
+}
+
+static ROLE_TO_TAG_MAP: Lazy> = Lazy::new(|| {
+ phf_map! {
+ "checkbox" => "input",
+ "button" => "button",
+ "heading" => "h1,h2,h3,h4,h5,h6",
+ "link" => "a,area",
+ "rowgroup" => "tbody,tfoot,thead",
+ "banner" => "header",
+ }
+});
+
+impl Rule for PreferTagOverRole {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ if let AstKind::JSXOpeningElement(jsx_el) = node.kind() {
+ if let Some(role_prop) = has_jsx_prop_lowercase(jsx_el, "role") {
+ Self::check_roles(role_prop, &ROLE_TO_TAG_MAP, &jsx_el.name, ctx);
+ }
+ }
+ }
+}
+#[test]
+fn test() {
+ use crate::tester::Tester;
+ let pass = vec![
+ "",
+ "",
+ "",
+ "",
+ "
",
+ "",
+ ];
+ let fail: Vec<&str> = vec![
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ r#""#,
+ ];
+ Tester::new_without_config(PreferTagOverRole::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/prefer_tag_over_role.snap b/crates/oxc_linter/src/snapshots/prefer_tag_over_role.snap
new file mode 100644
index 000000000000..4d6e1db4770f
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/prefer_tag_over_role.snap
@@ -0,0 +1,75 @@
+---
+source: crates/oxc_linter/src/tester.rs
+expression: prefer_tag_over_role
+---
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `input` over `role` attribute `checkbox`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `checkbox` to corresponding semantic HTML tag `input`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `button` over `role` attribute `button`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ──────────────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `button` to corresponding semantic HTML tag `button`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `input` over `role` attribute `checkbox`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ──────────────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `checkbox` to corresponding semantic HTML tag `input`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `h1,h2,h3,h4,h5,h6` over `role` attribute `heading`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ──────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `heading` to corresponding semantic HTML tag `h1,h2,h3,h4,h5,h6`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `a,area` over `role` attribute `link`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ───────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `link` to corresponding semantic HTML tag `a,area`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `tbody,tfoot,thead` over `role` attribute `rowgroup`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `rowgroup` to corresponding semantic HTML tag `tbody,tfoot,thead`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `input` over `role` attribute `checkbox`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `checkbox` to corresponding semantic HTML tag `input`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `input` over `role` attribute `checkbox`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `checkbox` to corresponding semantic HTML tag `input`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `input` over `role` attribute `checkbox`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ───────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `checkbox` to corresponding semantic HTML tag `input`.
+
+ ⚠ eslint-plugin-jsx-a11y(prefer-tag-over-role): Prefer `header` over `role` attribute `banner`.
+ ╭─[prefer_tag_over_role.tsx:1:1]
+ 1 │
+ · ─────────────
+ ╰────
+ help: Replace HTML elements with `role` attribute `banner` to corresponding semantic HTML tag `header`.
+
+