# Row Component — LLM Reference `Row` is a **horizontal arrangement primitive** that sits inside a `Container` column. It places its children side-by-side in a single ``, handles gap columns between them, and supports alignment, padding, background styling, borders, links, and mobile stacking. It does **not** control email-level width — that is the job of `Container`. --- ## Position in the Document Tree ``` DocumentTree └── Section ← full-width band └── Container ← column layout & email width └── Row ← horizontal child arrangement └── Image / Text / Button / ... ``` A `Row` lives inside a `Container` (or another component that acts as a column). Unlike `Container`, a `Row` does not manage fixed vs. full width at the email level — it fills whatever space its parent column provides. --- ## ComponentNode JSON Structure ```json { "id": "unique-string-id", "type": "RowComponent", "config": { "gap": "16px", "justifyContent": "center", "alignItems": "center", "fillWidth": false, "layoutColumns": "equal", "padding": "12px", "backgroundColor": "#ffffff", "borderRadius": "8px", "border": { "width": "1px", "style": "solid", "color": "#e5e7eb" }, "backgroundImage": { "src": "https://example.com/bg.jpg", "repeat": "no-repeat", "size": "cover", "position": "center center" }, "width": "100%", "height": "64px", "innerLink": { "type": "url", "url": "https://example.com", "target": "_blank" }, "mobile": { "wrap": true, "justifyContent": "center", "alignItems": "start" } }, "children": [] } ``` --- ## `RowConfig` — Full Property Reference ### Layout | Property | Type | Example | Description | |---|---|---|---| | `gap` | `string` | `"16px"` | Horizontal space between children. Rendered as a gap ``. Also used as vertical gap between stacked children on mobile. | | `justifyContent` | `"start" \| "center" \| "end"` | `"center"` | Horizontal alignment of children within the row | | `alignItems` | `"start" \| "center" \| "end"` | `"center"` | Vertical alignment of children within the row | | `width` | `string` | `"100%"` | Width of the outer row table. Defaults to `100%`. | | `height` | `string` | `"80px"` | Fixed height applied to all table levels | | `fillWidth` | `boolean` | `true` | See **`fillWidth` vs `layoutColumns`** section below | | `layoutColumns` | `"equal" \| string[]` | `"equal"` | Controls per-child `` width distribution. See below. | ### Visual | Property | Type | Example | Description | |---|---|---|---| | `padding` | `string` | `"16px 24px"` | Inner padding applied inside the border wrapper | | `backgroundColor` | `string` | `"#f9fafb"` | Background fill color | | `borderRadius` | `string` | `"8px"` | Rounds all corners (also clips background) | | `border` | `BorderConfig` | See below | Border on the outer wrapper table | | `backgroundImage` | `BackgroundImageType` | See below | Background image for the row | ### Interactivity | Property | Type | Description | |---|---|---| | `innerLink` | `IInnerLink` | Wraps the entire row in an `` tag. See **Link Support** section. | ### Mobile | Property | Type | Description | |---|---|---| | `mobile.wrap` | `boolean` | When `true`, children stack vertically on mobile. Gap is injected as a spacer between stacked children. | | `mobile.justifyContent` | `"start" \| "center" \| "end"` | Overrides `justifyContent` on mobile | | `mobile.alignItems` | `"start" \| "center" \| "end"` | Overrides `alignItems` on mobile | --- ## `fillWidth` vs `layoutColumns` — Critical Distinction These two properties both control how the content table expands, but they serve different purposes and can be combined. ### `fillWidth` Controls whether the inner content table uses `width: 100%` or `width: auto`. | `fillWidth` | Content table width | Use case | |---|---|---| | `false` / `undefined` (default) | `width: auto` | Children shrink-wrap to natural size. Use for icon rows, button rows, social links. | | `true` | `width: 100%` | Children get a constrained box — text can wrap correctly in Outlook. Use for rows mixing text + images. | ### `layoutColumns` Controls how each child `` is sized. When set, also forces `table-layout: fixed` and `width: 100%` on the content table (regardless of `fillWidth`). | Value | Behavior | |---|---| | `undefined` (default) | No width is set on child ``s — original shrink-wrap behavior | | `"equal"` | Each child gets `${100 / numChildren}%`. Works for any number of children. | | `string[]` | Explicit width per child (px, %, or mixed). Array **must** match children count exactly. | ```json // Equal: 3 children each get 33.33% "layoutColumns": "equal" // Manual: explicit per-child widths "layoutColumns": ["200px", "1fr", "80px"] // Mixed units are allowed — author is responsible for not overflowing "layoutColumns": ["60%", "40%"] ``` > **Note:** When using `%` values in `layoutColumns`, ensure they total ≤ 100% after accounting for any `gap`. Overflow behavior is undefined and client-dependent — the same trade-off accepted by MJML and Foundation for Emails. ### Decision Guide | Scenario | `fillWidth` | `layoutColumns` | |---|---|---| | Icon row, button row, social links | `false` | `undefined` | | Text + image side by side | `true` | `undefined` | | Equal-width columns (e.g. 3 product cards) | either | `"equal"` | | Asymmetric columns (e.g. 70/30 split) | either | `["70%", "30%"]` | | Fixed sidebar + fluid content | either | `["200px", "auto"]` | --- ## `BorderConfig` Specify either a **full border** (all sides) or **individual sides**. Do not mix both. ```json // Full border — all sides "border": { "width": "1px", "style": "solid", "color": "#e5e7eb" } // Per-side — other sides are explicitly set to "none" (Outlook-safe) "border": { "left": { "width": "4px", "style": "solid", "color": "#6366f1" } } ``` Supported side keys: `"top"`, `"right"`, `"bottom"`, `"left"`. Each side requires all three of: `width`, `style`, `color`. --- ## `backgroundImage` ```json "backgroundImage": { "src": "https://example.com/texture.jpg", "repeat": "no-repeat", "size": "cover", "position": "center center" } ``` | Property | Options | |---|---| | `repeat` | `"no-repeat"`, `"repeat"`, `"repeat-x"`, `"repeat-y"` | | `size` | `"auto"`, `"cover"`, `"contain"` | | `position` | Any valid CSS `background-position` string | Always pair with `backgroundColor` as a fallback for email clients that block images. --- ## Link Support (`innerLink`) When `innerLink` is set (and not `devMode`), the entire row is wrapped in an `` tag with `display: block`. The link is stripped in dev mode. ```json "innerLink": { "type": "url", "url": "https://example.com", "target": "_blank" } ``` | `type` | Required field | Rendered href | |---|---|---| | `"url"` | `url` | The URL as-is | | `"email"` | `email` | `mailto:{email}` | | `"phone"` | `phone` | `tel:{phone}` | | `"anchor"` | `anchor` | `#{anchor}` | | `"page_top"` | — | `#` | | `"page_bottom"` | — | `#bottom` | | `"none"` | — | No link rendered | `target` defaults to `"_blank"` if not specified. --- ## Mobile Stacking When `mobile.wrap: true` and there are 2+ children, the Row uses the same CSS-class-based stacking pattern as `Container` — compatible with Gmail's `@media` stripping. - Each child `` gets the `stack-td` class → forces `display: block; width: 100%` on mobile - Gap columns get `desktop-gap-column` → collapsed on mobile - A `mobile-gap-spacer` div is injected inside each child (except the last) → visible only on mobile, provides vertical spacing equal to `gap` ```json "config": { "gap": "16px", "mobile": { "wrap": true } } ``` > **Important:** `mobile.wrap` only activates stacking behavior. The `gap` value controls both horizontal gap (desktop) and vertical gap (mobile stack). If you want different spacing per breakpoint, that is not currently supported — one `gap` value serves both. --- ## React Usage ```tsx import Row, { RowConfig } from './Row'; const config: RowConfig = { gap: '16px', justifyContent: 'center', alignItems: 'center', fillWidth: false, }; ``` The component is memoized via `arePropsEqual`. Pass stable `config` objects to avoid re-renders. --- ## Row vs Container — Key Differences | | `Row` | `Container` | |---|---|---| | **Primary job** | Arrange children horizontally with alignment + gap | Control email column layout and fixed vs. full width | | **Width control** | Fills parent column (`width: 100%` default) | `"fixed"` (e.g. `600px`, centered) or `"full"` | | **Column sizing** | `layoutColumns` (`"equal"` or `string[]`) | `childrenConstraints` (`"equals"`, `"ratio"`, `"manual"`) | | **`fillWidth`** | ✓ Yes — shrink-wrap vs. full-width content table | ✗ No | | **Link wrapping** | ✓ Yes — `innerLink` wraps the whole row in `` | ✗ No | | **Mobile stacking** | ✓ Yes — `mobile.wrap` | ✓ Yes — `shouldWrap` | | **Border radius** | ✓ Yes | ✓ Yes | | **Background image** | ✓ Yes | ✓ Yes | | **`justifyContent`** | ✓ Yes — aligns children within the row | ✓ Yes — aligns the container table within its parent | --- ## Common Patterns & Examples ### 1. Icon/social link row (default shrink-wrap) ```json { "id": "row-social-icons", "type": "RowComponent", "config": { "gap": "12px", "justifyContent": "center", "alignItems": "center" }, "children": [] } ``` No `fillWidth`, no `layoutColumns` — children naturally size to their content. --- ### 2. Text + image side by side (fillWidth for Outlook wrapping) ```json { "id": "text-image-row", "type": "RowComponent", "config": { "gap": "24px", "alignItems": "center", "fillWidth": true, "layoutColumns": ["60%", "40%"], "mobile": { "wrap": true } }, "children": [] } ``` --- ### 3. Equal-width product cards (3-up) ```json { "id": "row-product-cards", "type": "RowComponent", "config": { "gap": "16px", "justifyContent": "center", "layoutColumns": "equal", "mobile": { "wrap": true } }, "children": [] } ``` --- ### 4. Single centered button ```json { "id": "cta-row", "type": "RowComponent", "config": { "justifyContent": "center", "padding": "8px 0" }, "children": [] } ``` No gap needed for a single child. No `layoutColumns` — button sizes to its natural width. --- ### 5. Clickable row (entire row is a link) ```json { "id": "link-row", "type": "RowComponent", "config": { "gap": "12px", "alignItems": "center", "fillWidth": true, "backgroundColor": "#f3f4f6", "padding": "16px", "borderRadius": "8px", "innerLink": { "type": "url", "url": "https://example.com/article", "target": "_blank" } }, "children": [] } ``` --- ### 6. Accent left-border row (callout style) ```json { "id": "callout-row", "type": "RowComponent", "config": { "fillWidth": true, "padding": "16px 20px", "backgroundColor": "#f0f9ff", "border": { "left": { "width": "4px", "style": "solid", "color": "#0ea5e9" } } }, "children": [] } ``` --- ### 7. Fixed-height banner row with background image ```json { "id": "banner-row", "type": "RowComponent", "config": { "height": "200px", "justifyContent": "center", "alignItems": "center", "backgroundColor": "#0f172a", "backgroundImage": { "src": "https://example.com/banner.jpg", "repeat": "no-repeat", "size": "cover", "position": "center center" } }, "children": [] } ``` --- ## Data Bindings `Row` supports the `bindings` property on the `ComponentNode`, alongside `id`, `type`, and `config`. Bindings connect this component to the Data Layer for dynamic rendering. ```json { "id": "row-social-bindings", "type": "RowComponent", "bindings": { "dataList": "social_links", "itemAlias": "link", "visible": "user.showSocial === true", "propertyMap": { "config.backgroundColor": "link.brandColor" } }, "config": { "gap": "12px", "justifyContent": "center", "alignItems": "center" }, "children": [] } ``` | Property | Description | |---|---| | `dataList` | Dot-path to an array in the Data Layer. Makes `Row` a repeater — one row rendered per item. | | `itemAlias` | Name used by descendant components to reference the current iteration item (e.g. `"link"` → `"link.brandColor"`). | | `visible` | JS expression evaluated against the Data Layer. Hides the entire row when falsy. | | `propertyMap` | Maps `config` property paths to Data Layer paths (e.g. `"config.backgroundColor"` → `"link.brandColor"`). | --- ## Rules & Constraints - **`layoutColumns` array must match children count exactly.** If lengths differ, widths are silently not applied (undefined fallback). - **`layoutColumns` forces `table-layout: fixed` and `width: 100%`** on the content table, overriding `fillWidth`. You cannot have `layoutColumns` without a full-width content table. - **`gap` doubles as mobile spacer height** when `mobile.wrap: true`. A single value serves both orientations. - **`innerLink` is suppressed in `devMode`.** The row renders without the `` wrapper when dev mode is active. - **`borderRadius` clips backgrounds.** Setting `borderRadius` automatically applies `overflow: hidden` to the outer background ``, clipping background colors and images within rounded corners. - **Per-side borders set all other sides to `"none"`** explicitly to prevent Outlook Classic from rendering phantom black borders. Do not rely on default/inherited border values when using per-side borders. - **`height` is propagated to all nested tables** via both the `height` CSS property and the HTML `height` attribute for Outlook Classic compatibility. - **`mobile.wrap` requires 2+ children** to have any effect. A single-child row is never stacked.