WIP: Custom CSS is silently converted into a setting that renders nothing

Environment

Bricks 2.4-beta2
WordPress 7.0.2
MCP Adapter 0.5.0
Client @automattic/mcp-wordpress-remote

Summary

When an element’s _cssCustom contains a rule whose selector targets that same element with a structural pseudo-class, the MCP normalizer rewrites it into an equivalent-looking Bricks setting and drops the CSS. The resulting setting is never emitted as CSS, so the rule silently disappears from the rendered page.

Observed with:

#brxe-ztdfpi:nth-child(2n) {
  flex-direction: row-reverse
}

which became:

"_direction:nth-child(2n)": "row-reverse"

The page’s generated stylesheet then contained zero occurrences of row-reverse. The layout effect — alternating the left/right order of every second item in a loop — was gone.

Evidence

The conversion is not a one-off, and it cannot be undone through the same API. Sending the correct CSS back while explicitly deleting the bogus setting:

{
  "_cssCustom": "#brxe-ztdfpi:nth-child(2n) {\n  flex-direction: row-reverse\n}",
  "_direction:nth-child(2n)": null
}

returns, from dryRun:

"normalization": {
  "changed": true,
  "changedSettingsKeys": ["_cssCustom", "_direction:nth-child(2n)"]
},
"element": {
  "settings": { "_direction:nth-child(2n)": "row-reverse" }
}

_cssCustom is discarded again and the dead setting is recreated. There is no way to store that rule on that element through MCP.

Scope: only self-targeting selectors

A rule doing the same job from the parent is left completely alone:

#brxe-usntku > div:nth-child(2n) {
  flex-direction: row-reverse;
}

This survives verbatim and renders correctly. Only #brxe-<own-id>:pseudo is captured by the converter. Two loops on the same site, written the two different ways, behaved differently after identical MCP writes — one kept its alternating layout, the other lost it.

Steps to reproduce

  1. Add a Block with a query loop so it repeats.
  2. Give that block custom CSS targeting itself:
    #brxe-<blockId>:nth-child(2n) { flex-direction: row-reverse }
  3. Save in the builder and confirm the alternating layout renders.
  4. Through the MCP server, call bricks/update-element on that block — or on any element, since a page-wide save is enough.
  5. Reload the frontend.

Expected

_cssCustom is stored as written and keeps rendering.

Actual

_cssCustom is replaced by _direction:nth-child(2n), no CSS is generated for it, and the alternating layout is lost. Nothing in the response marks this as destructive — changedSettingsKeys lists the keys, but does not say the rule will stop rendering.

Impact

Custom CSS is where users put exactly the things Bricks controls cannot express, so silently rewriting it is high-risk by nature. Here the visible result was a design detail — a deliberately non-monotonous alternating grid — reverting to a uniform layout on a live page, with no error anywhere.

Suggested fix

Either emit CSS for _direction:<pseudo> settings so the conversion is lossless, or stop converting _cssCustom altogether. Given that custom CSS is a deliberate escape hatch, leaving it untouched seems the safer default. If the conversion is kept, it should be verified round-trip: convert only when the resulting setting demonstrably produces the same CSS.

Workaround

Move the rule to the parent element and target the children, which the converter ignores:

#brxe-<parentId> > div:nth-child(2n) {
  flex-direction: row-reverse;
}

Hi @matutino,

Thank you for the report, this has been added to our internal bug tracker.

1 Like

Follow-up: narrowing down when the conversion is lossy

I hit this converter again on a different site and, this time, in a case where it behaved correctly. Comparing the two runs narrows the bug considerably, so I think it’s worth adding here.

Environment: Bricks 2.4-beta2 · WordPress 7.0.3 · MCP Adapter 0.5.0 · client @automattic/mcp-wordpress-remote

Three cases, same converter

I sent three different _cssCustom values through bricks/set-page-elements / bricks/update-element and then read each element back with bricks/get-page-elements and rendered it with bricks/render-elements.

Case A — property has a matching control, plain selector → converted, and the output is correct.

Sent, on a block with _display: "grid":

"_cssCustom": "#brxe-jgrid1 { align-content: center; }"

Stored as:

"_alignContentGrid": "center"

Rendered CSS:

#brxe-jgrid1 {display: grid; grid-template-columns: 1.2fr 0.8fr; grid-gap: 36px 64px; align-content: center}

The _cssCustom key is gone, but the declaration survives. No visual regression.

Case B — property has no matching control → left alone.

Sent:

"_cssCustom": "#brxe-jh1001 {\n  text-wrap: balance;\n}"

Read back verbatim, unchanged, and emitted as written. text-wrap has no Bricks control, so the converter correctly did not touch it.

Case C — the original report: control exists, but the selector carries a pseudo-class → converted into a key Bricks cannot emit.

#brxe-ztdfpi:nth-child(2n) { flex-direction: row-reverse }

became "_direction:nth-child(2n)": "row-reverse", and the generated stylesheet contained zero occurrences of row-reverse.

What this suggests

The converter looks up a property → control mapping and rewrites the declaration as a setting. That is sound on its own — cases A and B show it succeeding and correctly abstaining. The failure in case C is specifically that it carries the selector’s pseudo-class into the setting key and produces {controlKey}:{pseudoClass}, without checking that the pseudo-class is one Bricks can actually emit for that control.

On this install, bricks/list-pseudo-classes returns:

[":hover", ":active", ":focus", ":before", ":after"]

:nth-child(2n) is not in that list, and {controlKey}:{pseudoClass} only round-trips for entries that are. So a targeted fix would be: before converting, verify the selector’s pseudo-class (if any) is in the configured pseudo-class list, and leave the rule as custom CSS otherwise. That keeps the useful behaviour in cases A and B and removes the silent data loss in case C.

A smaller, separate point

Even when the conversion is correct (case A), it is still lossy at the API level: the _cssCustom string an author sent is not what they read back, and there is no way to ask for the original. For anyone diffing element JSON or syncing it from an external source, an element can appear to have “changed” on its own between a write and the next read. Worth documenting even if the conversion itself stays.