diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index 172393b5f2a..6dd013d556c 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -143,6 +143,10 @@ Ensures `disableScreenReaderOutput` is set when `EuiToolTip` content matches `Eu
Ensure `EuiIconTip` is used rather than ``, as it provides better accessibility and improved support for assistive technologies.
+### `@elastic/eui/no-unnamed-radio-group`
+
+Ensure that all radio input components (`EuiRadio`, `EuiRadioGroup`) have a `name` attribute. The `name` attribute is required for radio inputs to be grouped correctly, allowing users to select only one option from a set. Without a `name`, radios may not behave as expected and can cause accessibility issues for assistive technologies.
+
## Testing
### Running unit tests
diff --git a/packages/eslint-plugin/changelogs/upcoming/8929.md b/packages/eslint-plugin/changelogs/upcoming/8929.md
new file mode 100644
index 00000000000..ec96548d180
--- /dev/null
+++ b/packages/eslint-plugin/changelogs/upcoming/8929.md
@@ -0,0 +1 @@
+- Added new `no-unnamed-radio-group` rule.
diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts
index 32054ddccb2..ca7d25bf06c 100644
--- a/packages/eslint-plugin/src/index.ts
+++ b/packages/eslint-plugin/src/index.ts
@@ -25,6 +25,7 @@ import { RequireAriaLabelForModals } from './rules/a11y/require_aria_label_for_m
import { ConsistentIsInvalidProps } from './rules/a11y/consistent_is_invalid_props';
import { ScreenReaderOutputDisabledTooltip } from './rules/a11y/sr_output_disabled_tooltip';
import { PreferEuiIconTip } from './rules/a11y/prefer_eui_icon_tip';
+import { NoUnnamedRadioGroup } from './rules/a11y/no_unnamed_radio_group';
const config = {
rules: {
@@ -35,6 +36,7 @@ const config = {
'consistent-is-invalid-props': ConsistentIsInvalidProps,
'sr-output-disabled-tooltip': ScreenReaderOutputDisabledTooltip,
'prefer-eui-icon-tip': PreferEuiIconTip,
+ 'no-unnamed-radio-group' : NoUnnamedRadioGroup,
},
configs: {
recommended: {
@@ -47,6 +49,7 @@ const config = {
'@elastic/eui/consistent-is-invalid-props': 'warn',
'@elastic/eui/sr-output-disabled-tooltip': 'warn',
'@elastic/eui/prefer-eui-icon-tip': 'warn',
+ '@elastic/eui/no-unnamed-radio-group': 'warn',
},
},
},
diff --git a/packages/eslint-plugin/src/rules/a11y/no_unnamed_radio_group.test.ts b/packages/eslint-plugin/src/rules/a11y/no_unnamed_radio_group.test.ts
new file mode 100644
index 00000000000..f2737bd6ccf
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/a11y/no_unnamed_radio_group.test.ts
@@ -0,0 +1,99 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import dedent from 'dedent';
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import { NoUnnamedRadioGroup } from './no_unnamed_radio_group';
+
+const languageOptions = {
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+};
+
+const ruleTester = new RuleTester();
+
+ruleTester.run('no-unnamed-radio-group', NoUnnamedRadioGroup, {
+ valid: [
+ {
+ code: dedent`
+ const MyComponent = () => (
+
+ )
+ `,
+ languageOptions,
+ },
+ {
+ code: dedent`
+ const MyComponent = () => (
+
+ )
+ `,
+ languageOptions,
+ },
+ {
+ code: dedent`
+ const MyComponent = () => (
+
Not a radio
+ )
+ `,
+ languageOptions,
+ },
+ {
+ code: dedent`
+ const MyComponent = () => (
+
+ )
+ `,
+ languageOptions,
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ const MyComponent = () => (
+
+ )
+ `,
+ languageOptions,
+ errors: [
+ {
+ messageId: 'missingRadioName',
+ data: { component: 'EuiRadio' },
+ },
+ ],
+ },
+ {
+ code: dedent`
+ const MyComponent = () => (
+
+ )
+ `,
+ languageOptions,
+ errors: [
+ {
+ messageId: 'missingRadioName',
+ data: { component: 'EuiRadioGroup' },
+ },
+ ],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/src/rules/a11y/no_unnamed_radio_group.ts b/packages/eslint-plugin/src/rules/a11y/no_unnamed_radio_group.ts
new file mode 100644
index 00000000000..af3bd77cf5f
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/a11y/no_unnamed_radio_group.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ESLintUtils } from '@typescript-eslint/utils';
+
+const radioComponents = ['EuiRadio', 'EuiRadioGroup'];
+
+export const NoUnnamedRadioGroup = ESLintUtils.RuleCreator.withoutDocs({
+ create(context) {
+ return {
+ JSXOpeningElement(node) {
+ if (
+ node.name.type === 'JSXIdentifier' &&
+ radioComponents.includes(node.name.name)
+ ) {
+ const hasNameAttr = node.attributes.some(
+ (attr) =>
+ attr.type === 'JSXAttribute' &&
+ attr.name.type === 'JSXIdentifier' &&
+ attr.name.name === 'name'
+ );
+
+ if (!hasNameAttr) {
+ context.report({
+ node,
+ messageId: 'missingRadioName',
+ data: { component: node.name.name },
+ });
+ }
+ }
+ },
+ };
+ },
+ meta: {
+ type: 'problem',
+ docs: {
+ description:
+ 'Ensure that all radio input components have a `name` attribute. The `name` attribute is required for radio inputs to be grouped correctly, allowing users to select only one option from a set. Without a `name`, radios may not behave as expected and can cause accessibility issues for assistive technologies.',
+ },
+ schema: [],
+ messages: {
+ missingRadioName:
+ '{{ component }} must have a `name` attribute. The `name` attribute is required for radio inputs to be grouped correctly, ensuring only one option can be selected and improving accessibility for all users.',
+ },
+ },
+ defaultOptions: [],
+});