diff --git a/README.md b/README.md
index e77a809efe..56e7c3f670 100644
--- a/README.md
+++ b/README.md
@@ -375,6 +375,7 @@ module.exports = [
| [no-unused-prop-types](docs/rules/no-unused-prop-types.md) | Disallow definitions of unused propTypes | | | | | |
| [no-unused-state](docs/rules/no-unused-state.md) | Disallow definitions of unused state | | | | | |
| [no-will-update-set-state](docs/rules/no-will-update-set-state.md) | Disallow usage of setState in componentWillUpdate | | | | | |
+| [padding-lines-between-tags](docs/rules/padding-lines-between-tags.md) | Enforce no padding lines between tags for React Components | | | π§ | | |
| [prefer-es6-class](docs/rules/prefer-es6-class.md) | Enforce ES5 or ES6 class for React Components | | | | | |
| [prefer-exact-props](docs/rules/prefer-exact-props.md) | Prefer exact proptype definitions | | | | | |
| [prefer-read-only-props](docs/rules/prefer-read-only-props.md) | Enforce that props are read-only | | | π§ | | |
diff --git a/docs/rules/padding-lines-between-tags.md b/docs/rules/padding-lines-between-tags.md
new file mode 100644
index 0000000000..f25fed53f1
--- /dev/null
+++ b/docs/rules/padding-lines-between-tags.md
@@ -0,0 +1,127 @@
+# Enforce no padding lines between tags for React Components (`react/padding-lines-between-tags`)
+
+π§ This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
+
+
+Require or disallow newlines between sibling tags in React.
+
+## Rule Details Options
+
+```json
+{
+ "padding-line-between-tags": [
+ "error",
+ [{ "blankLine": "always", "prev": "*", "next": "*" }]
+ ]
+}
+```
+
+This rule requires blank lines between each sibling HTML tag by default.
+
+A configuration is an object which has 3 properties; `blankLine`, `prev`, and `next`. For example, `{ blankLine: "always", prev: "br", next: "div" }` means βone or more blank lines are required between a `br` tag and a `div` tag.β You can supply any number of configurations. If a tag pair matches multiple configurations, the last matched configuration will be used.
+
+- `blankLine` is one of the following:
+ - `always` requires one or more blank lines.
+ - `never` disallows blank lines.
+ - `consistent` requires or disallows a blank line based on the first sibling element.
+- `prev` any tag name without brackets.
+- `next` any tag name without brackets.
+
+### Disallow blank lines between all tags
+
+`{ blankLine: 'never', prev: '*', next: '*' }`
+
+```jsx
+
+
+
+```
+
+### Require newlines after `
`
+
+`{ blankLine: 'always', prev: 'br', next: '*' }`
+
+```jsx
+
+
+
+```
+
+### Require newlines between `
` and `
`
+
+`{ blankLine: 'always', prev: 'br', next: 'img' }`
+
+```jsx
+
+
+
+ -
+
+ -
+
+
+
+ -
+
+
+
+
+```
+
+```jsx [Fixed]
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+```
+
+### Require consistent newlines
+
+`{ blankLine: 'consistent', prev: '*', next: '*' }`
+
+```jsx
+
+
+
+```
+
+## When Not To Use It
+
+If you are not using jsx.
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 11a4475ba2..2b17db2401 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -89,6 +89,7 @@ module.exports = {
'no-unused-state': require('./no-unused-state'),
'no-object-type-as-default-prop': require('./no-object-type-as-default-prop'),
'no-will-update-set-state': require('./no-will-update-set-state'),
+ 'padding-lines-between-tags': require('./padding-lines-between-tags'),
'prefer-es6-class': require('./prefer-es6-class'),
'prefer-exact-props': require('./prefer-exact-props'),
'prefer-read-only-props': require('./prefer-read-only-props'),
diff --git a/lib/rules/padding-lines-between-tags.js b/lib/rules/padding-lines-between-tags.js
new file mode 100644
index 0000000000..a979d988bb
--- /dev/null
+++ b/lib/rules/padding-lines-between-tags.js
@@ -0,0 +1,187 @@
+/**
+ * @fileoverview Enforce no padding lines between tags for React Components
+ * @author Alankar Anand
+ * Based on https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-line-between-tags.js
+ * https://github.com/jsx-eslint/eslint-plugin-react/issues/3554
+ */
+
+'use strict';
+
+const docsUrl = require('../util/docsUrl');
+const report = require('../util/report');
+const eslintUtil = require('../util/eslint');
+
+/**
+ * Split the source code into multiple lines based on the line delimiters.
+ * Copied from padding-line-between-blocks
+ * @param {string} text Source code as a string.
+ * @returns {string[]} Array of source code lines.
+ */
+
+function splitLines(text) {
+ return text.split(/\r\n|[\r\n\u2028\u2029]/gu);
+}
+
+function insertNewLine(tag, sibling, lineDifference) {
+ const endTag = tag.closingElement || tag.openingElement;
+
+ if (lineDifference === 1) {
+ report({
+ message: 'Unexpected blank line before {{endTag}}.',
+ loc: sibling && sibling.loc,
+ // @ts-ignore
+ fix(fixer) {
+ return fixer.insertTextAfter(tag, '\n');
+ },
+ });
+ } else if (lineDifference === 0) {
+ report({
+ message: 'Expected blank line before {{endTag}}.',
+ loc: sibling && sibling.loc,
+ // @ts-ignore
+ fix(fixer) {
+ const lastSpaces = /** @type {RegExpExecArray} */ (
+ /^\s*/.exec(eslintUtil.getSourceCode().lines[endTag.loc.start.line - 1])
+ )[0];
+
+ return fixer.insertTextAfter(endTag, `\n\n${lastSpaces}`);
+ },
+ });
+ }
+}
+
+function removeExcessLines(endTag, sibling, lineDifference) {
+ if (lineDifference > 1) {
+ let hasOnlyTextBetween = true;
+ for (
+ let i = endTag.loc && endTag.loc.start.line;
+ i < sibling.loc.start.line - 1 && hasOnlyTextBetween;
+ i++
+ ) {
+ hasOnlyTextBetween = !/^\s*$/.test(eslintUtil.getSourceCode().lines[i]);
+ }
+ if (!hasOnlyTextBetween) {
+ report({
+ messageId: 'never',
+ loc: sibling && sibling.loc,
+ // @ts-ignore
+ fix(fixer) {
+ const start = endTag.range[1];
+ const end = sibling.range[0];
+ const paddingText = eslintUtil.getSourceCode().text.slice(start, end);
+ const textBetween = splitLines(paddingText);
+ let newTextBetween = `\n${textBetween.pop()}`;
+ for (let i = textBetween.length - 1; i >= 0; i--) {
+ if (!/^\s*$/.test(textBetween[i])) {
+ newTextBetween = `${i === 0 ? '' : '\n'}${
+ textBetween[i]
+ }${newTextBetween}`;
+ }
+ }
+ return fixer.replaceTextRange([start, end], `${newTextBetween}`);
+ },
+ });
+ }
+ }
+}
+
+function checkNewLine(configureList) {
+ const firstConsistentBlankLines = new Map();
+
+ const reverseConfigureList = [].concat(configureList).reverse();
+
+ return (node) => {
+ if (!node.parent.parent) {
+ return;
+ }
+
+ const endTag = node.closingElement || node.openingElement;
+
+ if (!node.parent.children) {
+ return;
+ }
+ const lowerSiblings = node.parent.children
+ .filter(
+ (element) => element.type === 'JSXElement' && element.range !== node.range
+ )
+ .filter((sibling) => sibling.range[0] - endTag.range[1] >= 0);
+
+ if (lowerSiblings.length === 0) {
+ return;
+ }
+ const closestSibling = lowerSiblings[0];
+
+ const lineDifference = closestSibling.loc.start.line - endTag.loc.end.line;
+
+ const configure = reverseConfigureList.find(
+ (config) => (config.prev === '*'
+ || node.openingElement.name.name === config.prev)
+ && (config.next === '*'
+ || closestSibling.openingElement.name.name === config.next)
+ );
+
+ if (!configure) {
+ return;
+ }
+
+ let blankLine = configure.blankLine;
+
+ if (blankLine === 'consistent') {
+ const firstConsistentBlankLine = firstConsistentBlankLines.get(
+ node.parent
+ );
+ if (firstConsistentBlankLine == null) {
+ firstConsistentBlankLines.set(
+ node.parent,
+ lineDifference > 1 ? 'always' : 'never'
+ );
+ return;
+ }
+ blankLine = firstConsistentBlankLine;
+ }
+
+ if (blankLine === 'always') {
+ insertNewLine(node, closestSibling, lineDifference);
+ } else {
+ removeExcessLines(endTag, closestSibling, lineDifference);
+ }
+ };
+}
+
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ // eslint-disable-next-line eslint-plugin/prefer-message-ids
+ meta: {
+ docs: {
+ description: 'Enforce no padding lines between tags for React Components',
+ category: 'Stylistic Issues',
+ recommended: false,
+ url: docsUrl('padding-lines-between-tags'),
+ },
+ fixable: 'code',
+ schema: [
+ {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ blankLine: { enum: ['always', 'never', 'consistent'] },
+ prev: { type: 'string' },
+ next: { type: 'string' },
+ },
+ additionalProperties: false,
+ required: ['blankLine', 'prev', 'next'],
+ },
+ },
+ ],
+ },
+
+ create(context) {
+ const configureList = context.options[0] || [
+ { blankLine: 'always', prev: '*', next: '*' },
+ ];
+ return {
+ JSXElement: checkNewLine(configureList),
+ };
+ },
+};