The first two bridges carry data to JS. The third carries the editor's style
choices — background, spacing, border, navigation colors — to SCSS, and it does so
without a front-end dev ever touching PHP. A App\Fields\* class turns each editor
setting into either an inline CSS custom property or a utility class on the
element. The colocated SCSS reads those variables. That's the whole seam.
Fields emit --vars + classes
A Field is a PHP class under App\Fields\ with static render helpers. Border
emits one custom property for the chosen color:
public static function cssVars(array $data): string
{
return '--border-color: '.ColorSelect::getCssColor($data['color']).';';
}
Spacing emits utility classes instead — one per edge, encoding the toggle and
its size choice:
public static function spacingClasses($data)
{
// ...
if ($data['padding_top']) {
$classes[] = 'padding-top-'.$data['padding_top_size'];
} else {
$classes[] = 'padding-top-none';
}
// margin_top, padding_bottom, margin_bottom …
return implode(' ', $classes); // e.g. "has-spacing padding-top-lg margin-top-none …"
}
Background does both at once — a marker class when any layer exists, plus a block
of per-device custom properties:
public static function backgroundClasses(?array $data): string
{
return empty($data ?? []) ? '' : 'has-background';
}
public static function backgroundVars(?array $data, string $size = 'xxlarge'): string
{
// ...per device: desktop / tablet / mobile
$vars[] = "--bg-images--{$device}: ".implode(', ', $images).';';
$vars[] = "--bg-positions--{$device}: ".implode(', ', $positions).';';
$vars[] = "--bg-repeats--{$device}: ".implode(', ', $repeats).';';
$vars[] = "--bg-sizes--{$device}: ".implode(', ', $sizes).';';
return implode("\n\t\t", $vars);
}
The view stitches them onto the element
The Module's view calls the class helpers into the class and style attributes.
Classes go in class, custom properties go in style:
<section
class="module intro animate <?= Background::backgroundClasses($options['background']); ?> <?= Spacing::spacingClasses($advanced['spacing']); ?>"
style="
<?= Background::backgroundVars($options['background']); ?>
<?= Border::cssVars($options['border']); ?>
<?= Navigation::cssVars($options['navigation']); ?>
">
The rendered element carries everything the SCSS needs — has-background,
padding-top-lg, --bg-images--desktop: …, --border-color: … — with no
JavaScript in the loop.
The SCSS consumes the variables
The colocated stylesheet reads the emitted properties. The editor's choice reaches the pixel purely through the custom property:
.notched-border {
border-color: var(--border-color);
}
Spacing's utility classes hook into the rhythm system:
the Spacing Field's padding-* / margin-* classes override the Module's
default --vertical-* rhythm variables, so an editor's size choice retunes the
fluid spacing scale without new CSS.
Why custom properties, not compiled CSS. Editor settings are per-instance and
unbounded — you can't compile a class for every color a user might pick. A custom
property is the one CSS primitive that carries a runtime value into a static
stylesheet, so the SCSS is authored once against var(--border-color) and each
element supplies its own value inline. Utility classes handle the enumerable
choices (a fixed set of spacing sizes); custom properties handle the open-ended
ones (any color, any image).
This page is the conceptual seam only — Field emits, SCSS consumes. How the SCSS actually consumes each variable, and the rhythm/gutter scale behind Spacing, lives in Styling. Don't duplicate the consumption details here.
Related
- The rhythm system — the
--vertical-*scale the Spacing Field overrides. data-*options — the bridge for scalar config that reaches JS instead of SCSS.- Data islands — the bridge for collections and structured payloads.