diff --git a/docs/user-guide/rules/list.md b/docs/user-guide/rules/list.md
index 7d6f4ea482..30b5847210 100644
--- a/docs/user-guide/rules/list.md
+++ b/docs/user-guide/rules/list.md
@@ -41,6 +41,7 @@ Grouped first by the following categories and then by the [_thing_](http://apps.
### Declaration block
+- [`declaration-block-no-duplicate-custom-properties`](../../../lib/rules/declaration-block-no-duplicate-custom-properties/README.md): Disallow duplicate custom properties within declaration blocks.
- [`declaration-block-no-duplicate-properties`](../../../lib/rules/declaration-block-no-duplicate-properties/README.md): Disallow duplicate properties within declaration blocks.
- [`declaration-block-no-shorthand-property-overrides`](../../../lib/rules/declaration-block-no-shorthand-property-overrides/README.md): Disallow shorthand properties that override related longhand properties.
diff --git a/lib/rules/declaration-block-no-duplicate-custom-properties/README.md b/lib/rules/declaration-block-no-duplicate-custom-properties/README.md
new file mode 100644
index 0000000000..b384b0df0c
--- /dev/null
+++ b/lib/rules/declaration-block-no-duplicate-custom-properties/README.md
@@ -0,0 +1,40 @@
+# declaration-block-no-duplicate-custom-properties
+
+Disallow duplicate custom properties within declaration blocks.
+
+
+```css
+a { --custom-property: pink; --custom-property: orange; }
+/** ↑ ↑
+ * These duplicated custom properties */
+```
+
+This rule is case-sensitive.
+
+## Options
+
+### `true`
+
+The following patterns are considered violations:
+
+
+```css
+a { --custom-property: pink; --custom-property: orange; }
+```
+
+
+```css
+a { --custom-property: pink; background: orange; --custom-property: orange }
+```
+
+The following patterns are _not_ considered violations:
+
+
+```css
+a { --custom-property: pink; }
+```
+
+
+```css
+a { --custom-property: pink; --cUstOm-prOpErtY: orange; }
+```
diff --git a/lib/rules/declaration-block-no-duplicate-custom-properties/__tests__/index.js b/lib/rules/declaration-block-no-duplicate-custom-properties/__tests__/index.js
new file mode 100644
index 0000000000..8c428a0ea1
--- /dev/null
+++ b/lib/rules/declaration-block-no-duplicate-custom-properties/__tests__/index.js
@@ -0,0 +1,165 @@
+'use strict';
+
+const { messages, ruleName } = require('..');
+
+testRule({
+ ruleName,
+ config: [true],
+
+ accept: [
+ {
+ code: 'a { --custom-property: 1 }',
+ },
+ {
+ code: 'a { --custom-property: 1; --cUstOm-prOpErtY: 1 }',
+ },
+ {
+ code: 'a { --custom-property: 1; color: pink; --cUstOm-prOpErtY: 1 }',
+ },
+ {
+ code: 'a { color: var(--custom-property, --custom-property) }',
+ },
+ {
+ code: 'a { --custom-property: pink; @media { --custom-property: orange; } }',
+ description: 'nested',
+ },
+ {
+ code:
+ 'a { --custom-property: pink; @media { --custom-property: orange; &::before { --custom-property: black; } } }',
+ description: 'double nested',
+ },
+ {
+ code:
+ 'a { --custom-property: pink; { &:hover { --custom-property: orange; --cUstOm-prOpErtY: black; } } }',
+ description: 'spec nested',
+ },
+ {
+ code:
+ 'a { --cUstOm-prOpErtY: pink; { &:hover { --custom-property: orange; --cUstOm-prOpErtY: black; } } }',
+ description: 'spec nested',
+ },
+ ],
+
+ reject: [
+ {
+ code: 'a { --custom-property: 1; --custom-property: 2; }',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code: 'a { --custom-property: 1; color: pink; --custom-property: 1; }',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code: 'a { --custom-property: 1; --cUstOm-prOpErtY: 1; color: pink; --cUstOm-prOpErtY: 1; }',
+ message: messages.rejected('--cUstOm-prOpErtY'),
+ },
+ {
+ code:
+ 'a { --custom-property: pink; { &:hover { --custom-property: orange; --custom-property: black; } } }',
+ description: 'spec nested',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code:
+ 'a { --custom-property: pink; @media { --custom-property: orange; --custom-property: black; } }',
+ description: 'nested',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code:
+ '@media { --custom-property: orange; .foo { --custom-property: black; --custom-property: white; } }',
+ description: 'nested',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code:
+ 'a { --custom-property: pink; @media { --custom-property: orange; &::before { --custom-property: black; --custom-property: white; } } }',
+ description: 'double nested',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code:
+ 'a { --custom-property: pink; @media { --custom-property: orange; .foo { --custom-property: black; --custom-property: white; } } }',
+ description: 'double nested again',
+ message: messages.rejected('--custom-property'),
+ },
+ ],
+});
+
+testRule({
+ ruleName,
+ config: [true],
+ skipBasicChecks: true,
+ syntax: 'html',
+
+ accept: [
+ {
+ code: '',
+ },
+ {
+ code: '',
+ },
+ {
+ code:
+ '',
+ },
+ {
+ code: '',
+ },
+ {
+ code:
+ '',
+ },
+ ],
+
+ reject: [
+ {
+ code: '',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code:
+ '',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code: '',
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code:
+ '',
+ message: messages.rejected('--cUstOm-prOpErtY'),
+ },
+ ],
+});
+
+testRule({
+ ruleName,
+ config: [true],
+ skipBasicChecks: true,
+ syntax: 'css-in-js',
+
+ accept: [
+ {
+ code:
+ "import styled from 'react-emotion'\nexport default styled.div` --custom-property: pink; `;",
+ },
+ {
+ code:
+ "import styled from 'react-emotion'\nexport default styled.div` --custom-property: pink; --cUstOm-prOpErtY: pink; `;",
+ },
+ ],
+ reject: [
+ {
+ code:
+ "import styled from 'styled-components';\nexport default styled.div` --custom-property: pink; --custom-property: orange; `;",
+ message: messages.rejected('--custom-property'),
+ },
+ {
+ code:
+ "import styled from 'react-emotion'\nexport default styled.div` --custom-property: pink; --custom-property: orange; `;",
+ message: messages.rejected('--custom-property'),
+ },
+ ],
+});
diff --git a/lib/rules/declaration-block-no-duplicate-custom-properties/index.js b/lib/rules/declaration-block-no-duplicate-custom-properties/index.js
new file mode 100644
index 0000000000..59712cf56d
--- /dev/null
+++ b/lib/rules/declaration-block-no-duplicate-custom-properties/index.js
@@ -0,0 +1,61 @@
+// @ts-nocheck
+
+'use strict';
+
+const eachDeclarationBlock = require('../../utils/eachDeclarationBlock');
+const isCustomProperty = require('../../utils/isCustomProperty');
+const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty');
+const report = require('../../utils/report');
+const ruleMessages = require('../../utils/ruleMessages');
+const validateOptions = require('../../utils/validateOptions');
+
+const ruleName = 'declaration-block-no-duplicate-custom-properties';
+
+const messages = ruleMessages(ruleName, {
+ rejected: (property) => `Unexpected duplicate "${property}"`,
+});
+
+function rule(on) {
+ return (root, result) => {
+ const validOptions = validateOptions(result, ruleName, { actual: on });
+
+ if (!validOptions) {
+ return;
+ }
+
+ eachDeclarationBlock(root, (eachDecl) => {
+ const decls = new Set();
+
+ eachDecl((decl) => {
+ const prop = decl.prop;
+
+ if (!isStandardSyntaxProperty(prop)) {
+ return;
+ }
+
+ if (!isCustomProperty(prop)) {
+ return;
+ }
+
+ const isDuplicate = decls.has(prop);
+
+ if (isDuplicate) {
+ report({
+ message: messages.rejected(prop),
+ node: decl,
+ result,
+ ruleName,
+ });
+
+ return;
+ }
+
+ decls.add(prop);
+ });
+ });
+ };
+}
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+module.exports = rule;
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 51ef1353a8..ebbc17de38 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -72,6 +72,9 @@ const rules = {
'custom-property-pattern': importLazy(() => require('./custom-property-pattern'))(),
'declaration-bang-space-after': importLazy(() => require('./declaration-bang-space-after'))(),
'declaration-bang-space-before': importLazy(() => require('./declaration-bang-space-before'))(),
+ 'declaration-block-no-duplicate-custom-properties': importLazy(() =>
+ require('./declaration-block-no-duplicate-custom-properties'),
+ )(),
'declaration-block-no-duplicate-properties': importLazy(() =>
require('./declaration-block-no-duplicate-properties'),
)(),