From 0a825b05dc875da85a0429a87bd602348be463aa Mon Sep 17 00:00:00 2001
From: Igor <86978055+Poliklot@users.noreply.github.com>
Date: Wed, 3 Jun 2026 10:40:52 +0300
Subject: [PATCH] Preserve blank lines between JSX attributes (#19161)
---
changelog_unreleased/javascript/19161.md | 37 ++++
src/language-js/print/jsx.js | 15 +-
.../__snapshots__/format.test.js.snap | 171 ++++++++++++++++++
.../attribute-blank-lines.js | 38 ++++
.../jsx/attribute-blank-lines/format.test.js | 4 +
.../jsx/jsx/__snapshots__/format.test.js.snap | 32 ++++
.../tsx/__snapshots__/format.test.js.snap | 25 +++
.../typescript/tsx/attribute-blank-lines.tsx | 7 +
8 files changed, 328 insertions(+), 1 deletion(-)
create mode 100644 changelog_unreleased/javascript/19161.md
create mode 100644 tests/format/jsx/attribute-blank-lines/__snapshots__/format.test.js.snap
create mode 100644 tests/format/jsx/attribute-blank-lines/attribute-blank-lines.js
create mode 100644 tests/format/jsx/attribute-blank-lines/format.test.js
create mode 100644 tests/format/typescript/tsx/attribute-blank-lines.tsx
diff --git a/changelog_unreleased/javascript/19161.md b/changelog_unreleased/javascript/19161.md
new file mode 100644
index 000000000..ce2969c38
--- /dev/null
+++ b/changelog_unreleased/javascript/19161.md
@@ -0,0 +1,37 @@
+#### Preserve blank lines between JSX attributes (#19161 by @Poliklot)
+
+Prettier now keeps one existing blank line between JSX attributes, consistent with how it preserves blank lines in many other places. Multiple blank lines are still collapsed to one.
+
+
+```jsx
+// Input
+;
+
+// Prettier stable
+;
+
+// Prettier main
+;
+```
diff --git a/src/language-js/print/jsx.js b/src/language-js/print/jsx.js
index e8f50335d..9330dba0a 100644
--- a/src/language-js/print/jsx.js
+++ b/src/language-js/print/jsx.js
@@ -25,6 +25,7 @@ import { CommentCheckFlags, hasComment } from "../utilities/comments.js";
import { createTypeCheckFunction } from "../utilities/create-type-check-function.js";
import { getRaw } from "../utilities/get-raw.js";
import { isMeaningfulJsxText } from "../utilities/is-meaningful-jsx-text.js";
+import { isNextLineEmpty } from "../utilities/is-next-line-empty.js";
import { jsxWhitespace } from "../utilities/jsx-whitespace.js";
import {
isArrayExpression,
@@ -652,7 +653,19 @@ function printJsxOpeningElement(path, options, print) {
"<",
print("name"),
print("typeArguments"),
- indent(path.map(() => [attributeLine, print()], "attributes")),
+ indent(
+ path.map(
+ ({ isFirst, previous }) => [
+ isFirst
+ ? attributeLine
+ : isNextLineEmpty(previous, options)
+ ? [hardline, hardline]
+ : attributeLine,
+ print(),
+ ],
+ "attributes",
+ ),
+ ),
...printEndOfOpeningTag(node, options, nameHasComments),
],
{ shouldBreak },
diff --git a/tests/format/jsx/attribute-blank-lines/__snapshots__/format.test.js.snap b/tests/format/jsx/attribute-blank-lines/__snapshots__/format.test.js.snap
new file mode 100644
index 000000000..4b3630b6f
--- /dev/null
+++ b/tests/format/jsx/attribute-blank-lines/__snapshots__/format.test.js.snap
@@ -0,0 +1,171 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`attribute-blank-lines.js - {"singleAttributePerLine":true} format 1`] = `
+====================================options=====================================
+parsers: ["flow", "babel", "typescript"]
+singleAttributePerLine: true
+ printWidth: 80 (default) |
+=====================================input======================================
+const basic = (
+