---
title: Composition Rules for post_content
audience: content
source: us-core 8.45.2
generated: 2026-05-20 (manual)
---

# Composition Rules for `post_content`

> Cross-cutting rules for assembling valid `post_content` from UpSolution shortcodes. Per-shortcode "when to use / avoid" notes live in [`shortcodes.md`](shortcodes.md); ready-made blocks live in [`sections.md`](sections.md). This document covers the structural and syntactic invariants that span shortcodes.
>
> Conventions:
>
> - Code blocks use the `text` language tag — they are raw shortcode markup, **not** PHP or HTML.
> - "Leaf" = shortcode that cannot have children (e.g. `us_btn`, `us_text`, `us_image`). "Container" = shortcode that wraps children (e.g. `vc_row`, `vc_column`, `hwrapper`).
> - Tag prefixes are kept exactly as they must appear in `post_content`: `us_*` for UpSolution-native, `vc_*` for WPBakery-compatible tags. Do not invent variants.

---

## 1. Root structure

`post_content` is a flat string of top-level shortcodes. The **only** shortcode that may appear at the top level is `vc_row`. Every other shortcode must be nested inside a `vc_row → vc_column` chain.

Valid root:

```text
[vc_row][vc_column][us_text text="Hi"][/vc_column][/vc_row]
[vc_row][vc_column][us_btn label="Click"][/vc_column][/vc_row]
```

Invalid root (renders, but not as a section — alignment, backgrounds, responsive layout all break):

```text
[us_text text="Hi"]
[us_btn label="Click"]
```

A page is a **sequence of `vc_row` blocks**, one per visual section. There is no wrapping element above `vc_row` — concatenate rows directly.

---

## 2. Nesting graph

The builder enforces these parent/child constraints. Violating them produces a block that the builder cannot edit and that may render incorrectly.

| Shortcode | Allowed parents | Allowed children |
|-----------|-----------------|------------------|
| `vc_row` | `post_content` root only | `vc_column` only |
| `vc_column` | `vc_row` only | leaves, `vc_row_inner`, `hwrapper`, `vwrapper`, `vc_tta_tabs`, `vc_tta_tour`, `vc_tta_accordion` |
| `vc_row_inner` | `vc_column`, `vc_tta_section` | `vc_column_inner` only |
| `vc_column_inner` | `vc_row_inner` only | same as `vc_column` |
| `hwrapper`, `vwrapper` | any column-like container (`vc_column`, `vc_column_inner`, `vc_tta_section`) | leaves only (no nested containers, no other wrappers) |
| `vc_tta_tabs`, `vc_tta_tour`, `vc_tta_accordion` | `vc_column`, `vc_column_inner` | `vc_tta_section` only |
| `vc_tta_section` | `vc_tta_tabs`, `vc_tta_tour`, `vc_tta_accordion` only | leaves, `vc_row_inner`, `hwrapper`, `vwrapper` |
| leaves (`us_btn`, `us_text`, `us_iconbox`, …) | any container | none |

Practical consequences:

- **Need a nested grid inside a column?** Use `vc_row_inner` + `vc_column_inner`. Do not nest `vc_row` inside `vc_column` — `vc_row` is root-only and the builder will refuse to edit it.
- **Need to group a few inline elements with a gap?** Use `hwrapper` (horizontal) or `vwrapper` (vertical) inside the column. These accept leaves only.
- **Need tabs or an accordion?** A `vc_tta_tabs|tour|accordion` block contains one `vc_tta_section` per tab/panel. `vc_tta_section` cannot live anywhere else — placing it directly under `vc_column` produces an orphan.

### How a row's column layout is determined

A row's **desktop** column count and proportions are **derived from the `width` attribute on each child `vc_column`**, not from any attribute on the row itself. At render time us-core scans the row's children, collects their `width` values, and reduces them to the layout descriptor — so a row with three bare `[vc_column]` children renders as a **single full-width column**, no matter what `columns="3"` you set on the row.

Always put `width="<num>/<den>"` on every `vc_column` when the row has more than one column. The same rule applies to `vc_row_inner` → `vc_column_inner`.

| Intent | Markup |
|--------|--------|
| 2 equal columns | `[vc_column width="1/2"]…[/vc_column][vc_column width="1/2"]…[/vc_column]` |
| 3 equal columns | three `vc_column` with `width="1/3"` each |
| 4 equal columns | four `vc_column` with `width="1/4"` each |
| 1/3 + 2/3 | `[vc_column width="1/3"]…[/vc_column][vc_column width="2/3"]…[/vc_column]` |
| 2/5 + 3/5 | `[vc_column width="2/5"]…[/vc_column][vc_column width="3/5"]…[/vc_column]` |
| 1/4 + 1/2 + 1/4 | three columns with widths `1/4`, `1/2`, `1/4` |

Valid `width` values: `1/1`, `1/2`, `1/3`, `2/3`, `1/4`, `3/4`, `1/5`, `2/5`, `3/5`, `4/5`, `1/6`, `5/6`. Any other combination is treated as a custom CSS-grid layout (each width becomes a `1fr`-scaled track) — usually not what you want.

**Tablet and mobile column layouts** still come from row-level attributes (`laptops_columns`, `tablets_columns`, `mobiles_columns`) because there are no narrower-viewport equivalents on individual columns. See §7 for those.

Source of truth: `as_child` / `as_parent` / `is_container` keys in `plugins/us-core/config/elements/*.php`.

---

## 3. Attribute encoding

Shortcode attributes follow the WordPress shortcode syntax: `name="value"`. A few attribute *families* have extra encoding on top of that.

### 3.1 `link="..."` — link picker

Format:

```text
url:<urlencoded-url>|title:<title>|target:_blank|rel:nofollow|onclick:<js>
```

- `url:` is mandatory; the URL portion is **URL-encoded** (so `https://example.com/path?x=1` becomes `https%3A%2F%2Fexample.com%2Fpath%3Fx%3D1`, and an anchor `#signup` becomes `%23signup`).
- Other keys are optional. Separate keys with `|`.
- A double `||` skips a key (legacy form, still accepted): `url:<enc>||target:_blank`.

Examples:

```text
[us_btn label="Get started" link="url:%23signup"]
[us_btn label="Docs" link="url:https%3A%2F%2Fdocs.example.com|target:_blank|rel:nofollow"]
[us_btn label="Email" link="url:mailto%3Asupport%40example.com"]
```

**Dynamic targets** (post permalink, author archive, popup-image, "use a custom-field value as the URL", etc.) are picked from a per-shortcode enum and stored in a URL-encoded JSON variant of the same attribute (`{"type":"post"}`, `{"type":"popup_image"}`, `{"type":"custom_field|us_tile_link"}`, …). See [Dynamic Values → Link enums](element-dynamic-values.md#link-enums) for the full enum list per `link` parameter, and [Text tokens](element-dynamic-values.md#text-tokens) for `{{…}}` substitution in text / textarea / image-id attributes.

### 3.2 Strings with spaces, quotes, newlines

- Always quote values: `text="Hello world"` — bare `text=Hello world` does not parse.
- Inside a quoted value, **literal newlines** are allowed and meaningful — `us_itext`, for example, uses them to separate animated lines:

  ```text
  [us_itext texts="Build it
  Ship it
  Love it"]
  ```

- HTML entities (`&quot;`, `&amp;`) are not decoded inside attribute values. Use a different quoting strategy or move the content into `vc_column_text` (see §4).
- Backticks, percent signs, and curly braces have no special meaning in attribute values and need no escaping.

### 3.3 `css="…"` — per-element CSS rules

The `css="…"` attribute is available on **every** shortcode and holds a URL-encoded JSON document describing breakpoint-specific CSS overrides for that one element. It is the standard way to change colors, sizes, paddings, borders, shadows, transforms etc. on a specific element without touching theme CSS.

**Format — URL-encoded JSON, in double quotes:**

```text
css="%7B%22default%22%3A%7B%22color%22%3A%22%23ff0000%22%2C%22font-size%22%3A%2240px%22%7D%2C%22mobiles%22%3A%7B%22font-size%22%3A%2224px%22%7D%7D"
```

decodes to:

```json
{
  "default": { "color": "#ff0000", "font-size": "40px" },
  "mobiles": { "font-size": "24px" }
}
```

**Important** — unlike `columns_gap` (§3.4), the single-quoted raw JSON shortcut does **not** work for `css="…"`. The post-save extractor that builds the compiled stylesheet uses a regex that only matches `css="…"` with double quotes (`plugins/us-core/functions/post.php:320`). A single-quoted `css='{"…"}'` will produce the right CSS class on the element but the CSS rule itself will never be emitted, leaving the element unstyled. Always use the URL-encoded double-quoted form.

**JSON shape:**

- Top-level keys are breakpoint names: `default` (desktop), `laptops`, `tablets`, `mobiles`. Same semantics as §3.4 — all optional, missing keys leave the property at its CSS default.
- Each breakpoint's value is an object mapping CSS property names (kebab-case) to value strings.
- Property names **must use a hyphen** (`font-size`, not `font_size`). The set of allowed properties is fixed — anything outside the whitelist below is dropped silently.

**URL-encoding cheatsheet** (the characters that need encoding inside the attribute value):

| Char | Encoded |
|------|---------|
| `{`  | `%7B` |
| `}`  | `%7D` |
| `"`  | `%22` |
| `:`  | `%3A` |
| `,`  | `%2C` |
| `#`  | `%23` |
| ` `  (space) | `%20` |
| `/`  | `%2F` |
| `(`  | `%28` |
| `)`  | `%29` |

JSON syntax characters (`{`, `}`, `"`, `:`, `,`) appear in every value; numeric values and ASCII identifiers pass through unchanged. The decoder is `rawurldecode`, so `+` is **not** treated as a space — use `%20` for literal spaces (rare; mostly inside `transform`, `clip-path`, multi-value `box-shadow-*` etc.).

**Recipe — author in two steps:**

1. Write the rule as readable JSON first:
   ```json
   {"default":{"padding-top":"40px","padding-bottom":"40px","background-color":"_content_bg_alt"}}
   ```
2. URL-encode the entire string using the table above, then drop it into `css="…"`:
   ```text
   css="%7B%22default%22%3A%7B%22padding-top%22%3A%2240px%22%2C%22padding-bottom%22%3A%2240px%22%2C%22background-color%22%3A%22_content_bg_alt%22%7D%7D"
   ```

**Whitelist of CSS properties** (everything outside this list is silently dropped):

- **Text**: `color`, `text-align`, `font-size`, `line-height`, `letter-spacing`, `font-family`, `font-weight`, `text-transform`, `text-wrap`, `font-style`.
- **Background**: `background-color`, `background-image`, `background-position`, `background-size`, `background-blend-mode`, `background-repeat`, `background-attachment`, `backdrop-filter`.
- **Sizes**: `width`, `height`, `max-width`, `max-height`, `min-width`, `min-height`, `aspect-ratio`.
- **Spacing**: `margin-top`, `margin-right`, `margin-bottom`, `margin-left`, `padding-top`, `padding-right`, `padding-bottom`, `padding-left`. (Use the four sub-properties; the compiler auto-collapses to `padding`/`margin` shorthand when all four are equal.)
- **Border**: `border-radius`, `border-style`, `border-top-width`, `border-right-width`, `border-bottom-width`, `border-left-width`, `border-color`.
- **Position**: `position`, `top`, `right`, `bottom`, `left`, `z-index`.
- **Text shadow** (set all four — partial values get default-filled): `text-shadow-h-offset`, `text-shadow-v-offset`, `text-shadow-blur`, `text-shadow-color`.
- **Box shadow** (set all five — partial values get default-filled): `box-shadow-h-offset`, `box-shadow-v-offset`, `box-shadow-blur`, `box-shadow-spread`, `box-shadow-color`.
- **Overflow / clip**: `overflow`, `clip-path`.
- **Transformation**: `transform`, `transform-origin`.
- **Entry animation**: `animation-name`, `animation-delay`.

**Special-case values:**

- `background-image` takes a WordPress **media ID** (e.g. `"background-image": "123"`), not a `url(…)` string. The compiler resolves it to `url(<media URL>)` at render time. If you must inline a URL, the value can also be `"url(https://…)"` — but media ID is preferred.
- `font-family` accepts either a font name from Theme Options → Typography or one of the special values `body`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, which resolve to the theme's `--<tag>-font-family` CSS variables — use those when you want the element to inherit the typography role of a tag.
- Color values accept palette tokens (e.g. `_content_primary`, `_content_bg_alt`) in addition to literal `#hex` and `rgba()` — see §5 for the token list. Prefer tokens.
- `text-shadow-*` / `box-shadow-*` are set as separate sub-properties. The compiler joins them into the final `text-shadow` / `box-shadow` rule. Missing sub-values get sensible defaults (`color` → `currentColor`, offsets/blur/spread → `0`), so setting only `box-shadow-color` and `box-shadow-blur` is fine.
- `transform` and `clip-path` accept raw CSS function syntax: `"transform": "translateY(-50%) rotate(5deg)"`, `"clip-path": "polygon(25% 0%, 100% 0%, 75% 100%, 0% 100%)"`. Inner parentheses and commas inside the value must be URL-encoded as `%28`, `%29`, `%2C`.

**How it renders:**

Each unique `css="…"` value gets hashed into a class `us_custom_<crc32>` and the element receives that class. The corresponding CSS rule is compiled and stored on the post; on save WP injects it as a `<style>` tag. Two elements with byte-identical `css="…"` share the same class and the same compiled rule — useful for repeated cards, harmless either way.

**Examples:**

Red centered H2, smaller on mobile:

```text
[us_text text="Important notice" tag="h2" css="%7B%22default%22%3A%7B%22color%22%3A%22%23ff0000%22%2C%22text-align%22%3A%22center%22%2C%22font-size%22%3A%2240px%22%7D%2C%22mobiles%22%3A%7B%22font-size%22%3A%2224px%22%7D%7D"]
```

Column with extra vertical padding and an alt-palette background, less padding on mobile:

```text
[vc_column width="1/2" css="%7B%22default%22%3A%7B%22padding-top%22%3A%2260px%22%2C%22padding-bottom%22%3A%2260px%22%2C%22background-color%22%3A%22_content_bg_alt%22%7D%2C%22mobiles%22%3A%7B%22padding-top%22%3A%2230px%22%2C%22padding-bottom%22%3A%2230px%22%7D%7D"]
```

Image with rounded corners and a soft drop shadow:

```text
[us_image image="123" css="%7B%22default%22%3A%7B%22border-radius%22%3A%2212px%22%2C%22box-shadow-v-offset%22%3A%228px%22%2C%22box-shadow-blur%22%3A%2224px%22%2C%22box-shadow-color%22%3A%22rgba%280%2C0%2C0%2C0.15%29%22%7D%7D"]
```

**Anti-patterns:**

- **Single-quoted `css='{"…"}'`** — the class is added but no CSS is emitted; the element looks unchanged. Always double-quote and URL-encode.
- **Using a property name outside the whitelist** (e.g. `gap`, `display`, `flex-direction`) — silently dropped. If the layout requires it, restructure with the appropriate container shortcode (`hwrapper` for inline gap, `vc_row`/`vc_row_inner` columns for grids) instead.
- **Using underscores in property names** (`font_size`, `background_color`) — silently dropped. Use kebab-case.
- **Stripping `css="…"` from a pre-built section** to "clean it up" — see §6.

### 3.4 Responsive single-value attributes (`columns_gap`, sizes, paddings)

A class of size-like attributes (`columns_gap` on rows, font sizes on text, gaps on wrappers, several `us_bg_*` props) can vary per breakpoint. The value lives in a **single attribute** with one of two equivalent shapes — there is **no** separate `tablets_<param>` / `mobiles_<param>` form for them.

**Form A — scalar (use when one value works at every viewport):**

```text
columns_gap="2rem"
```

Applies to every breakpoint. When columns stack on mobile, the same scalar becomes the vertical gap.

**Form B — per-breakpoint JSON (use when the value must change per viewport):**

A JSON object with breakpoint keys. **Wrap the attribute in single quotes** so the JSON's double quotes do not collide with the shortcode parser:

```text
columns_gap='{"default":"6rem","mobiles":"1rem"}'
```

Equivalent URL-encoded form inside double quotes (less readable, no advantage over single-quoted raw JSON):

```text
columns_gap="%7B%22default%22%3A%226rem%22%2C%22mobiles%22%3A%221rem%22%7D"
```

**JSON shape:**

- Keys are breakpoint names. Allowed keys, in render order:
  - `default` — desktop (≥1381px by default)
  - `laptops` — 1025–1380px
  - `tablets` — 601–1024px
  - `mobiles` — ≤600px
- All keys are optional. A missing key falls back to the param's CSS default (which is whatever us-core / Theme Options set globally for that property, **not** the value of the next-wider breakpoint).
- Values are CSS unit strings — `3rem`, `40px`, `100%`, `0`. Use the same units across breakpoints when you can; mixing rem / px works but is harder to reason about.

**Common patterns:**

```text
columns_gap='{"default":"4rem","mobiles":"1.5rem"}'           # wide on desktop, tight on mobile
columns_gap='{"mobiles":"0rem"}'                              # only override mobile; desktop keeps the 3rem default
columns_gap='{"default":"6rem","tablets":"3rem","mobiles":"1rem"}'  # three steps
size='{"default":"4rem","tablets":"3rem","mobiles":"2rem"}'   # responsive font size on us_text
```

**Authoring tips:**

- Set `default` whenever you set any other breakpoint — otherwise the desktop value falls back to a CSS default that may not be what you want.
- Mobile gaps generally want to be 30–50% of desktop gaps. A `6rem` desktop gap looks correct, but the same `6rem` between stacked mobile blocks creates huge dead space.
- Other row params accept the same JSON shape: `gap`, `content_placement`, several `us_bg_*` sizes. Per-shortcode `Key parameters` notes mark which ones are responsive.

The few row attributes that genuinely accept per-breakpoint overrides as **separate** attributes (`laptops_columns`, `tablets_columns`, `mobiles_columns`) are listed in §7; that pattern is specific to column-count and does not generalise to any other param.

### 3.5 Group JSON attributes (`items`, `orderby_items`, `responsive`, `tax_query`, `meta_query`, …)

A second class of attributes takes a **JSON array or array-of-objects** describing a *group* of sub-elements rather than a single value. Examples across the in-scope shortcode set:

| Shortcode(s) | Attribute(s) |
|--------------|--------------|
| `us_list_filter` | `items` (filter rows) |
| `us_list_order` | `orderby_items` (sort options) |
| `us_post_list` / `us_product_list` / their `_carousel` siblings | `tax_query`, `meta_query` |
| `us_post_carousel` / `us_product_carousel` / `us_content_carousel` / `us_term_carousel` / `us_user_carousel` | `responsive` (per-breakpoint overrides) |
| `us_socials` | `items` (social entries) |
| `us_cform` | `items` (form fields) |

**Required shape — URL-encoded JSON inside double quotes:**

```text
items="%5B%7B%22source%22%3A%22us_portfolio_category%22%2C%22selection_type%22%3A%22radio%22%2C%22values_as_btn%22%3A%221%22%2C%22values_btn_cols%22%3A%22auto%22%7D%5D"
```

decodes to:

```json
[{"source":"us_portfolio_category","selection_type":"radio","values_as_btn":"1","values_btn_cols":"auto"}]
```

**Do not use single-quoted raw JSON** — `items='[{"source":"…"}]'` *looks* readable but breaks reliably in `post_content`. WordPress's `wptexturize` filter runs over the post body and treats the opening apostrophe of the attribute value as an opening typographic quote; every straight quote that follows (the inner `"` of each JSON key/value pair, the closing apostrophe of the attribute itself, **and** the quotes of every attribute that comes after on the same shortcode tag) is converted to `&#8217;` / `&#8221;`. The shortcode regex then can't recognize any of those attributes — they're silently dropped and the element falls back to its defaults. Symptoms an agent will see:

- `us_list_filter` rendering with `data-name="post_type"` and `type_checkbox` even though you set `source="us_portfolio_category"` and `selection_type="radio"` — those settings never reached the renderer.
- The form's classes contain literal curly-quote entities: `layout_&#8221;hor&#8221; mod_&#8221;no_titles&#8221;`.
- Trailing attributes (`list_selector_to_filter`, `list_to_filter`, etc.) appear *outside* the closing `]` of the shortcode tag in the rendered HTML, as escaped text.
- Carousels collapsing to default `items="3"` on every viewport because `responsive='[{…}]'` was dropped.

This affects single-quoted JSON in **any** group attribute, regardless of nesting context (verified inside `us_hwrapper`, inside `vc_column` directly, etc.).

The section-template snapshots in [`sections/`](sections/) use the URL-encoded form for `us_socials items=` exclusively (23 occurrences across `co.md`, `ab.md`, `fo.md`, `po.md`, …; zero single-quoted) — that is the de-facto correct form across the codebase.

**Encoding recipe** — same character table as §3.3, plus brackets:

| Char | Encoded |
|------|---------|
| `[`  | `%5B` |
| `]`  | `%5D` |
| `{`  | `%7B` |
| `}`  | `%7D` |
| `"`  | `%22` |
| `:`  | `%3A` |
| `,`  | `%2C` |

Author in two steps: write the JSON readably first, then URL-encode the whole string and drop into `<attr>="…"`. PowerShell one-liner: `[System.Uri]::EscapeDataString($rawJson)`. PHP: `rawurlencode($rawJson)`. JS: `encodeURIComponent(rawJson)`.

**Contrast with §3.4:** the short per-breakpoint scalar maps used by `columns_gap` / `size` / `gap` / `content_placement` (`'{"default":"6rem","mobiles":"1rem"}'`) work in single-quoted form because they're short and the template library uses them widely without issue. Any JSON whose top level is `[ … ]` — i.e. an *array*, regardless of length — falls under §3.5 and needs URL-encoded form.

**Related us-core bug to know about:** when an `items` entry has `"values_as_btn":"1"`, `us_list_filter` reads `values_btn_cols` from that entry without a null-coalesce (`plugins/us-core/templates/elements/list_filter.php:802`), so a PHP Notice prints inline next to the rendered filter when `WP_DEBUG_DISPLAY` is on. Set `"values_btn_cols":"auto"` to silence it — that value drives the CSS class `btns_in_row_auto` which lays the buttons out in a natural flex row (the matching CSS lives in `common/css/elements/grid-filter.css`). Do **not** pick a numeric value like `"4"` "just to set something" — that produces a fixed-column grid (`btns_in_row_4` → `grid-template-columns: repeat(4, 1fr)`) and your three category buttons render with an empty fourth cell. Only set a number when you genuinely want a fixed grid.

---

## 4. HTML inside shortcode values

By default, shortcode attribute values are **plain text** — HTML tags inside an attribute are stripped before rendering. There are three exceptions, in increasing order of permissiveness.

### 4.1 Inline-HTML-aware attributes

A handful of label/title/text attributes accept a small allowlist of inline tags. Anything outside the allowlist is silently stripped.

| Shortcode | Attribute | Allowed tags | Source |
|-----------|-----------|--------------|--------|
| `us_text` | `text` | `<br>`, `<code>`, `<i>`, `<small>`, `<span>`, `<strong>`, `<sub>`, `<sup>` | `plugins/us-core/templates/elements/text.php:45` |
| `us_btn` | `label` | `<br>`, `<code>`, `<i>`, `<small>`, `<span>`, `<strong>`, `<sub>`, `<sup>` | `common/functions/helpers.php:1044` (`us_get_btn()`) |
| `us_ibanner` | `title` | `<strong>`, `<br>` | `plugins/us-core/templates/elements/ibanner.php:53` |
| `us_separator` | `text` | `<strong>`, `<br>` | `plugins/us-core/templates/elements/separator.php:71` |
| `us_flipbox` | `front_title`, `back_title` | `<br>` | `plugins/us-core/templates/elements/flipbox.php:131,183` |

Typical uses:

```text
[us_btn label="Save <strong>30%</strong> today"]
[us_text text="Built for<br>builders" tag="h1"]
[us_text text="H<sub>2</sub>O reacts with CO<sub>2</sub>"]
[us_text text="Call <code>do_shortcode()</code> from a template"]
[us_separator text="<strong>OR</strong>"]
```

Practical limits:

- Tags outside the allowlist are stripped. `us_btn label="Get <em>started</em>"` renders as `Get started` (no italics) — `<em>` is not on the list; use `<i>`. Likewise `<b>` becomes plain text — use `<strong>`.
- Block-level tags (`<p>`, `<div>`, `<h1>`–`<h6>`, `<ul>`, `<li>`, `<a>`) are stripped from every attribute on every shortcode. For block content or hyperlinks use `vc_column_text` (see §4.3).
- Attribute values are still WordPress shortcode strings: literal double quotes inside the value need to be encoded as `&quot;` or `&#34;`. A nested `<span style="color:#f00;">…</span>` is technically allowed (the `<span>` is on the `us_text` / `us_btn` list), but inline CSS in shortcode attributes is brittle — prefer the shortcode's own `color` / `dynamic_color` params.

### 4.2 Attributes that strip all HTML

Every label/title/text param **not** listed in §4.1 strips all tags before output. This covers most other titles across in-scope shortcodes (`us_dropdown > title`, `us_popup > title`, and similar). When you need formatting and no allowlisted attribute exists, put the rich-text portion in an adjacent `vc_column_text`.

### 4.3 `vc_column_text` — full HTML body

`vc_column_text` is the only shortcode whose **body** accepts arbitrary HTML (run through WordPress's `widget_text_content` filter — paragraphs, lists, blockquotes, inline links, images, headings, inline styles). Use it whenever the content needs anything beyond §4.1's inline tags or needs hyperlinks within body copy:

```text
[vc_column_text]
<p>First paragraph with an <a href="https://example.com">inline link</a>.</p>
<ul><li>Bullet one</li><li>Bullet two</li></ul>
<blockquote>A pull quote.</blockquote>
[/vc_column_text]
```

Guidelines:

- For headings, prefer `us_text tag="h1"…"h6"` over a raw `<h2>` inside `vc_column_text` — `us_text` exposes typographic params (`font`, `size`, alignment) and integrates with site typography settings.
- Standalone CTA links should be `us_btn`, not `<a class="button">` inside `vc_column_text`.
- Inside any *other* shortcode body, raw `<p>` / `<ul>` / `<h2>` tags are stripped — wrap that prose in a `vc_column_text` next to the other shortcodes inside the same column.

---

## 5. Color and style references (palette tokens)

Many color params accept symbolic theme-palette tokens in place of literal HEX/RGBA. These resolve to the active theme's CSS variables on render and survive site re-theming. Prefer them over hard-coded colors.

Common tokens (defined in `plugins/us-core/config/theme-options.php`, sub-section "Content Colors" / "Alternate Content Colors"):

| Token | What it maps to |
|-------|-----------------|
| `_content_primary` | brand/primary accent color |
| `_content_link` | link color |
| `_content_link_hover` | link hover color |
| `_content_heading` | headings color |
| `_content_bg` | body/section background |
| `_content_bg_alt` | alternate section background |
| `_alt_content_*` | the same set under the "alternate" color scheme (rows with `color_scheme="alternate"`) |

Row-level `color_scheme` switches the active palette for everything inside the row:

| `color_scheme` value | Effect |
|----------------------|--------|
| empty (default) | content colors |
| `alternate` | alternate content colors (`_alt_content_*` tokens) |
| `primary` | primary background, white text |
| `secondary` | secondary background, white text |
| `footer-top` | alternate footer palette |
| `footer-bottom` | main footer palette |

Practical use:

```text
[vc_row color_scheme="alternate"]
  [vc_column]
    [us_text text="On alt background" css_color="_alt_content_heading"]
    [us_btn label="Action" color="_content_primary"]
  [/vc_column]
[/vc_row]
```

Literal `#rrggbb` and `rgba(…)` are accepted everywhere a color param exists, but they break site theming — pick a token first, fall back to a literal only when the desired color has no token.

---

## 6. Reusing pre-built sections

Section blocks from [`sections.md`](sections.md) are **self-contained `vc_row` blocks**. To use one:

1. Pick a category file under [`sections/`](sections/) and copy the `text` block under any `### <template-id>` entry.
2. Paste it into `post_content` as-is. The block already starts with `[vc_row …]` and ends with `[/vc_row]`, so it slots in at the top level next to other rows — no extra wrapping.
3. Edit text, labels, link URLs, image IDs in place. Preserve the section's `css="…"` attributes unless you have a reason to restyle — they carry the visual design. When you do want to override them, edit the JSON inside following the recipe in §3.3.

### Residual `columns="…"` on row blocks

Section blocks routinely carry both `columns="…"` on the `vc_row` and `width="…"` on each `vc_column`. The row attribute is leftover UI state written by the live builder; at render time it is overwritten by the children's widths (see §2). Both being present is harmless. When **editing** a section to change its column count, edit the child `width` values — touching `columns` on the row has no effect.

### `use:placeholder` markers

The snapshot reproduces shortcode markup verbatim from `us.api`. Image references that ship without a default media ID appear as the literal token `use:placeholder` (or its URL-encoded form `use%3Aplaceholder` inside `css="…"` blobs):

```text
[us_image image="use:placeholder" has_ratio="1" ratio="3x2"]
```

Two options:

- **Replace** with a real WordPress media ID (`image="1234"`) — preferred for production content.
- **Leave as-is** — the front-end us-core renderer substitutes `use:placeholder` with a generic placeholder image at render time. Useful for drafting before assets are ready.

### Stacking multiple sections

A page can be assembled by concatenating sections, one after another:

```text
[vc_row …]…[/vc_row]   <!-- Intro -->
[vc_row …]…[/vc_row]   <!-- Features -->
[vc_row …]…[/vc_row]   <!-- Pricing -->
[vc_row …]…[/vc_row]   <!-- CTA -->
[vc_row …]…[/vc_row]   <!-- Footer-style block -->
```

No wrapper, no separator — just adjacent rows. Visual spacing between them is controlled by each row's `height` and padding params.

When two adjacent rows would share the same background, give them contrasting `color_scheme` values (or set `color_scheme=""` and `color_scheme="alternate"` on alternate rows) to produce visible separation.

---

## 7. Responsive overrides

Column layout and several other layout params accept per-breakpoint overrides. The breakpoints, from widest to narrowest:

| Breakpoint | Param suffix | Default screen width |
|------------|--------------|----------------------|
| desktop | derived from child `vc_column` `width` values (see §2) | wider than `1380px` |
| laptops | `laptops_columns` on the row | up to `1380px` |
| tablets | `tablets_columns` on the row | up to `1024px` |
| mobiles | `mobiles_columns` on the row | up to `600px` |

Rules:

- **Desktop layout comes from child column widths** (see §2). The narrower-viewport overrides below are row attributes.
- **`inherit` cascades down**: `laptops_columns="inherit"` reuses the desktop value (i.e. whatever was computed from child widths), `tablets_columns="inherit"` reuses laptop (which may itself be desktop), and so on. Omitting the attribute is equivalent to `inherit`.
- **Mobile defaults to one column** for `vc_row` / `vc_row_inner`: `mobiles_columns="1"` is the assumed default unless explicitly overridden. A three-column row on desktop becomes single-column on mobile automatically.
- **Override only what changes** — do not set `laptops_columns="3" tablets_columns="3" mobiles_columns="3"` to "keep three columns everywhere"; let the desktop layout cascade and add `ignore_columns_stacking="1"` only if you genuinely need three columns on mobile (rare, usually icon grids).
- The `laptops_columns` / `tablets_columns` / `mobiles_columns` attributes accept the same enum as the auto-computed desktop `columns`: `1`, `2`, `3`, `4`, `5`, `6`, `1-2`, `2-1`, `1-3`, `3-1`, `1-4`, `4-1`, `1-5`, `5-1`, `2-3`, `3-2`, `1-2-1`, `1-3-1`, `1-4-1`, `inherit`, `custom`.

Example — three desktop columns, two on tablet, one on mobile:

```text
[vc_row tablets_columns="2" mobiles_columns="1"]
  [vc_column width="1/3"]…[/vc_column]
  [vc_column width="1/3"]…[/vc_column]
  [vc_column width="1/3"]…[/vc_column]
[/vc_row]
```

Other params that accept similar suffixes: `columns_gap`, `content_placement`, several `us_bg_*` props. Check the per-shortcode entry in [`shortcodes.md`](shortcodes.md) for the responsive flag on each parameter.

---

## 8. Cross-shortcode anti-patterns

Patterns that look reasonable in isolation but break composition. Per-shortcode anti-patterns (e.g. "do not set `columns='1-1'`") live in the corresponding entry in [`shortcodes.md`](shortcodes.md); the list here covers issues that span shortcodes.

- **Setting `columns="3"` on a `vc_row` and omitting `width="…"` on the child columns.** The row's column count is computed from child widths; the row attribute is overwritten at render time. Bare `[vc_column]` defaults to `width="1/1"`, so the row collapses to a single full-width column regardless of `columns="3"`. Always put `width="<num>/<den>"` on each `vc_column` in a multi-column row. Same trap on `vc_row_inner` + `vc_column_inner`. See §2.
- **Inventing per-breakpoint attribute variants** like `tablets_columns_gap`, `mobiles_columns_gap`, `tablets_size`, `mobiles_padding`. With the exception of `laptops_columns` / `tablets_columns` / `mobiles_columns` on rows (§7), us-core does not split responsive params into separate attributes. For per-breakpoint values, use the JSON shape on the single base attribute: `columns_gap='{"default":"6rem","mobiles":"1rem"}'`. See §3.4.
- **Nesting `vc_row` inside `vc_column`** to make a sub-grid. `vc_row` is root-only. Use `vc_row_inner` + `vc_column_inner` instead.
- **Using `vc_tta_section` outside a tabs/accordion/tour container**. It is a child element only; on its own it renders nothing useful.
- **Single-quoted `css='{…}'` or unencoded JSON inside `css="…"`** — the unique class gets added but the CSS rule is never compiled (the post-save extractor only matches URL-encoded JSON in double quotes). See §3.3 for the correct shape and recipe.
- **Single-quoted raw JSON in a group attribute** — `items='[{…}]'`, `orderby_items='[{…}]'`, `responsive='[{…}]'`, `tax_query='[{…}]'`, etc. Looks readable in examples but `wptexturize` mangles the leading apostrophe into a curly quote and corrupts every subsequent quote on the same shortcode tag; the shortcode regex then drops every attribute and the element falls back to its defaults (filter shows the wrong taxonomy, carousel collapses to default `items`, etc.). Always use URL-encoded JSON in double quotes — see §3.5.
- **Putting a leaf directly under `vc_row`** (`[vc_row][us_btn …][/vc_row]`). `vc_row` accepts `vc_column` only; the button does not render. Wrap in a column.
- **Mixing raw HTML and `vc_column_text` inconsistently** in the same column. Pick one: either pack the full block of prose into a single `vc_column_text`, or use a sequence of `us_text` / `us_btn` / `us_iconbox` leaves. Interleaving stray `<p>` tags between leaves places them outside any rendered block.
- **Using literal HEX colors when a palette token exists.** A literal `#1a73e8` ignores the site's theme and breaks when the theme is re-skinned. Prefer `_content_primary`, `_content_link`, etc. (see §5).
- **Hard-coding all four breakpoints when one would do.** `inherit` is the default. Setting `laptops_columns="3" tablets_columns="3" mobiles_columns="3"` reads as deliberate uniformity but masks the intent; just use `columns="3" mobiles_columns="1"` (or whatever the real mobile target is).
- **Stripping `css="…"` from a section template to "clean it up".** That attribute carries the design (paddings, type scale, background colors). Without it the block reverts to a plain stack and loses the visual identity of the section.
- **Inventing new tag names.** The set of valid tags is fixed by the registered shortcodes — there is no `us_section`, no `us_hero`, no `us_cta_box`. Use the actual tags listed in [`shortcodes.md`](shortcodes.md).
