diff --git a/README.md b/README.md
index 02f1ed5c0..229faccc1 100644
--- a/README.md
+++ b/README.md
@@ -165,6 +165,7 @@ will be evaluated as an `h3`. If no `polymorphicPropName` is set, then the compo
| [no-aria-hidden-on-focusable](docs/rules/no-aria-hidden-on-focusable.md) | Disallow `aria-hidden="true"` from being set on focusable elements. | | | |
| [no-autofocus](docs/rules/no-autofocus.md) | Enforce autoFocus prop is not used. | ☑️ 🔒 | | |
| [no-distracting-elements](docs/rules/no-distracting-elements.md) | Enforce distracting elements are not used. | ☑️ 🔒 | | |
+| [no-duplicate-ids](docs/rules/no-duplicate-ids.md) | Disallow duplicate ids. | ☑️ 🔒 | | |
| [no-interactive-element-to-noninteractive-role](docs/rules/no-interactive-element-to-noninteractive-role.md) | Interactive elements should not be assigned non-interactive roles. | ☑️ 🔒 | | |
| [no-noninteractive-element-interactions](docs/rules/no-noninteractive-element-interactions.md) | Non-interactive elements should not be assigned mouse or keyboard event listeners. | ☑️ 🔒 | | |
| [no-noninteractive-element-to-interactive-role](docs/rules/no-noninteractive-element-to-interactive-role.md) | Non-interactive elements should not be assigned interactive roles. | ☑️ 🔒 | | |
diff --git a/__tests__/src/rules/no-duplicate-ids.js b/__tests__/src/rules/no-duplicate-ids.js
new file mode 100644
index 000000000..71a0bc6c1
--- /dev/null
+++ b/__tests__/src/rules/no-duplicate-ids.js
@@ -0,0 +1,44 @@
+/**
+ * @fileoverview Disallow duplicate ids.
+ * @author Chris Ng
+ */
+
+// -----------------------------------------------------------------------------
+// Requirements
+// -----------------------------------------------------------------------------
+
+import { RuleTester } from 'eslint';
+import parserOptionsMapper from '../../__util__/parserOptionsMapper';
+import parsers from '../../__util__/helpers/parsers';
+import rule from '../../../src/rules/no-duplicate-ids';
+
+// -----------------------------------------------------------------------------
+// Tests
+// -----------------------------------------------------------------------------
+
+const ruleTester = new RuleTester();
+
+const expectedError = (idValue) => ({
+ message: `Duplicate ID "${idValue}" found. ID attribute values must be unique.`,
+ type: 'JSXOpeningElement',
+});
+
+const expectedJSXError = (idValue) => ({
+ message: `Duplicate ID "${idValue}" found. ID attribute JSX experssions must be unique.`,
+ type: 'JSXOpeningElement',
+});
+
+ruleTester.run('no-duplicate-ids', rule, {
+ valid: parsers.all([].concat(
+ { code: '
' },
+ { code: '' },
+ { code: '
' },
+ { code: '' },
+ )).map(parserOptionsMapper),
+ invalid: parsers.all([].concat(
+ { code: '', errors: [expectedError('chris')] },
+ { code: '', errors: [expectedJSXError('chris')] },
+ { code: '
', errors: [expectedError('chris')] },
+ { code: '', errors: [expectedJSXError('chris')] },
+ )).map(parserOptionsMapper),
+});
diff --git a/docs/rules/no-duplicate-ids.md b/docs/rules/no-duplicate-ids.md
new file mode 100644
index 000000000..50f016a80
--- /dev/null
+++ b/docs/rules/no-duplicate-ids.md
@@ -0,0 +1,32 @@
+# jsx-a11y/no-duplicate-ids
+
+💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
+
+
+
+Enforces that no `id` attributes are reused. This rule does a basic check to ensure that `id` attribute values are not the same. In the case of a JSX expression, it checks that no `id` attributes reuse the same expression.
+
+## Rule details
+
+This rule takes no arguments.
+
+### Succeed
+
+```jsx
+
+
+
+
+```
+
+### Fail
+
+```jsx
+
+
+
+
+```
+
+## Accessibility guidelines
+- [WCAG 4.1.1](https://www.w3.org/WAI/WCAG21/Understanding/parsing.html)
diff --git a/src/index.js b/src/index.js
index 7b931fe34..0a6c856da 100644
--- a/src/index.js
+++ b/src/index.js
@@ -29,6 +29,7 @@ module.exports = {
'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'),
'no-autofocus': require('./rules/no-autofocus'),
'no-distracting-elements': require('./rules/no-distracting-elements'),
+ 'no-duplicate-ids': require('./rules/no-duplicate-ids'),
'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'),
'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'),
'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'),
@@ -116,6 +117,7 @@ module.exports = {
'jsx-a11y/no-access-key': 'error',
'jsx-a11y/no-autofocus': 'error',
'jsx-a11y/no-distracting-elements': 'error',
+ 'jsx-a11y/no-duplicate-ids': 'error',
'jsx-a11y/no-interactive-element-to-noninteractive-role': [
'error',
{
@@ -273,6 +275,7 @@ module.exports = {
'jsx-a11y/no-access-key': 'error',
'jsx-a11y/no-autofocus': 'error',
'jsx-a11y/no-distracting-elements': 'error',
+ 'jsx-a11y/no-duplicate-ids': 'error',
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
'jsx-a11y/no-noninteractive-element-interactions': [
'error',
diff --git a/src/rules/no-duplicate-ids.js b/src/rules/no-duplicate-ids.js
new file mode 100644
index 000000000..46ad3dc97
--- /dev/null
+++ b/src/rules/no-duplicate-ids.js
@@ -0,0 +1,55 @@
+/**
+ * @fileoverview Disallow duplicate ids.
+ * @author Chris Ng
+ */
+
+// ----------------------------------------------------------------------------
+// Rule Definition
+// ----------------------------------------------------------------------------
+
+import { getProp, getPropValue } from 'jsx-ast-utils';
+import { generateObjSchema } from '../util/schemas';
+
+const schema = generateObjSchema();
+
+export default {
+ meta: {
+ docs: {
+ url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/no-duplicate-ids.md',
+ description: 'Disallow duplicate ids.',
+ },
+ schema: [schema],
+ },
+
+ create(context) {
+ const idsUsedSet = new Set();
+ const jsxExperissionIDsUsedSet = new Set();
+
+ return {
+ JSXOpeningElement(node) {
+ const { attributes } = node;
+ const idProp = getProp(attributes, 'id');
+ const idValue = getPropValue(idProp);
+
+ // Special case if id is assigned using JSXExpressionContainer
+ if (idProp && idProp.type === 'JSXAttribute' && idProp.value.type === 'JSXExpressionContainer') {
+ if (jsxExperissionIDsUsedSet.has(idValue)) {
+ context.report({
+ node,
+ message: `Duplicate ID "${idValue}" found. ID attribute JSX experssions must be unique.`,
+ });
+ } else {
+ jsxExperissionIDsUsedSet.add(idValue);
+ }
+ } else if (idsUsedSet.has(idValue)) {
+ context.report({
+ node,
+ message: `Duplicate ID "${idValue}" found. ID attribute values must be unique.`,
+ });
+ } else {
+ idsUsedSet.add(idValue);
+ }
+ },
+ };
+ },
+};