Repository: mjmlio/mjml

Issue:
Add a way to add spacing between sections
**Is your feature request related to a problem? Please describe.**
Something similar was requested in #876, which amounts to trying to add an `mj-spacer` in between 2 `mj-section` elements.

This is not guaranteed to work (even if it appears to work in my VSCode preview), but I don't think there's currently a good way to add spacing in between sections otherwise.

**Describe the solution you'd like**
I'd like to be able to (somehow) add spacing between sections. Perhaps even something like this (not exactly this terminology) could work:
```xml
<mj-list spacing="10">
  <mj-section />
  <mj-section />
</mj-list>
```

**Describe alternatives you've considered**
It's possible to add a `border-bottom="10px solid #fff"` to the bottom of the section, but that's not a great solution either.

**Additional context**
<img width="1335" height="577" alt="Image" src="https://github.com/user-attachments/assets/5331daaf-e90b-4996-acf8-7b33a6213ef6" />


Discussion:
Hi ! I think the best "bulletproof" solution is to add an empty section with a padding. I'm not sure about how outlook will behave...

It's not pretty but this is what i'm using : 
```
<mj-section padding-bottom="40px"><mj-column><mj-text>&nbsp;</mj-text></mj-column></mj-section>
```

Maybe we could add an attribute to `mj-wrapper` to add a flex-like "gap" between section that would do that 🤔 
```
<mj-wrapper gap="20px"> 
  <mj-section />
  <mj-section />
</mj-wrapper>

```
I looked at `mj-wrapper` to see if it could do that exact thing, actually! So that sounds like the perfect place for it.



Patch:
diff --git a/packages/mjml-section/src/index.js b/packages/mjml-section/src/index.js
index 80be2eaba..ca05da8ea 100644
--- a/packages/mjml-section/src/index.js
+++ b/packages/mjml-section/src/index.js
@@ -47,6 +47,7 @@ export default class MjSection extends BodyComponent {
     return {
       ...this.context,
       containerWidth: `${box}px`,
+      gap: this.getAttribute('gap'),
     }
   }
 
@@ -57,6 +58,8 @@ export default class MjSection extends BodyComponent {
 
     const hasBorderRadius = this.hasBorderRadius()
 
+    const isFirstSection = this.props.index === 0
+
     const background = this.getAttribute('background-url')
       ? {
           background: this.getBackground(),
@@ -102,6 +105,7 @@ export default class MjSection extends BodyComponent {
         'max-width': containerWidth,
         'border-radius': this.getAttribute('border-radius'),
         ...(hasBorderRadius && { overflow: 'hidden' }),
+        'margin-top': !isFirstSection ? this.context.gap : undefined,
       },
       innerDiv: {
         'line-height': '0',
@@ -195,12 +199,21 @@ export default class MjSection extends BodyComponent {
     return borderRadius !== '' && typeof borderRadius !== 'undefined'
   }
 
+  hasGap() {
+    const { gap } = this.context
+    return gap != null && gap !== ''
+  }
+
   renderBefore() {
     const { containerWidth } = this.context
     const bgcolorAttr = this.getAttribute('background-color')
       ? { bgcolor: this.getAttribute('background-color') }
       : {}
 
+    const isFirstSection = this.props.index === 0
+
+    const hasGap = this.hasGap()
+
     return `
       <!--[if mso | IE]>
       <table
@@ -211,9 +224,12 @@ export default class MjSection extends BodyComponent {
           cellspacing: '0',
           class: suffixCssClasses(this.getAttribute('css-class'), 'outlook'),
           role: 'presentation',
-          style: { width: `${containerWidth}` },
+          style: {
+            width: `${containerWidth}`,
+            'padding-top': !isFirstSection ? this.context.gap : undefined,
+          },
           width: parseInt(containerWidth, 10),
-          ...bgcolorAttr,
+          ...(!hasGap && { ...bgcolorAttr }),
         })}
       >
         <tr>
diff --git a/packages/mjml-wrapper/README.md b/packages/mjml-wrapper/README.md
index 3df88b917..5acaf8722 100644
--- a/packages/mjml-wrapper/README.md
+++ b/packages/mjml-wrapper/README.md
@@ -64,6 +64,7 @@ border-right        | string      | css border format              | n/a
 border-top          | string      | css border format              | n/a
 css-class           | string      | class name, added to the root HTML element created | n/a
 full-width          | string      | make the wrapper full-width    | n/a
+gap                 | px          | applies a vertical gap between child mj-sections   | n/a
 padding             | px          | supports up to 4 parameters    | 20px 0
 padding-bottom      | px          | section bottom offset          | n/a
 padding-left        | px          | section left offset            | n/a
diff --git a/packages/mjml-wrapper/src/index.js b/packages/mjml-wrapper/src/index.js
index b0335f8a2..217758a01 100644
--- a/packages/mjml-wrapper/src/index.js
+++ b/packages/mjml-wrapper/src/index.js
@@ -4,6 +4,17 @@ import { suffixCssClasses } from 'mjml-core'
 export default class MjWrapper extends MjSection {
   static componentName = 'mj-wrapper'
 
+  static allowedAttributes = {
+    gap: 'unit(px)',
+  }
+
+  getChildContext() {
+    return {
+      ...this.context,
+      gap: this.getAttribute('gap'),
+    }
+  }
+
   renderWrappedChildren() {
     const { children } = this.props
     const { containerWidth } = this.context


Test patch:
diff --git a/packages/mjml/test/wrapper-gap.test.js b/packages/mjml/test/wrapper-gap.test.js
new file mode 100644
index 000000000..874200f95
--- /dev/null
+++ b/packages/mjml/test/wrapper-gap.test.js
@@ -0,0 +1,55 @@
+const chai = require('chai')
+const { load } = require('cheerio')
+const mjml = require('../lib')
+
+describe('mj-wrapper gap', function () {
+  it('should render correct gap values in CSS style values on children mj-section', function () {
+    const input = `
+    <mjml>
+      <mj-body>
+        <mj-wrapper gap="20px" css-class="my-wrapper" background-color="#000"> 
+          <mj-section css-class="my-section" background-color="#f45e43" padding="10px">
+            <mj-column>
+              <mj-text>Section 1</mj-text>
+            </mj-column>
+          </mj-section>
+          <mj-section css-class="my-section" background-color="#ccc" padding="10px">
+            <mj-column>
+              <mj-text>Section 2</mj-text>
+            </mj-column>
+          </mj-section>
+          <mj-section css-class="my-section" background-color="#333" padding="10px">
+            <mj-column>
+              <mj-text color="#fff">Section 3</mj-text>
+            </mj-column>
+          </mj-section>
+        </mj-wrapper>
+      </mj-body>
+    </mjml>
+    `
+
+    const { html } = mjml(input)
+
+    const $ = load(html)
+
+    // gap values should be correct
+    chai
+      .expect(
+        $('.my-section')
+          .map(function getAttr() {
+            const str = $(this).attr('style')
+            const substr = 'margin-top:'
+
+            if (str.includes(substr)) {
+              const start = $(this).attr('style').indexOf(substr) + 11
+              const end = $(this).attr('style').indexOf(';', start)
+              return $(this).attr('style').substring(start, end)
+            }
+            return undefined
+          })
+          .get(),
+        'Gap in CSS style values on mj-wrapper',
+      )
+      .to.eql(['20px', '20px'])
+  })
+})