WebProCMS rows render as <section> blocks with their own background colors and link colors. The editor's design tab lets an editor change a section's whole color story — background, text color, link color, link hover color, heading colors — without writing CSS, picking from a curated dropdown of presets defined under Dashboard → Design → Sections.
The presets ship with light, dark, primary, secondary, accent, and surface/tinted variations — enough that breaking up a page visually (white section → light gray section → dark section → primary section) is a single dropdown change. Each preset's link colors are tuned to read well on its background, so contrast and brand consistency come for free.
The problem
Long pages need visual rhythm. Three white sections stacked on top of each other read as one big block of content. Putting a light-gray section between them, or a dark section every few rows, gives the eye somewhere to land and makes the page feel composed rather than dumped.
The naive way to do this is to let editors type Tailwind classes into a "section classes" textarea — bg-zinc-800 text-white, bg-primary text-tone-50, and so on. That works for the editor who knows Tailwind, but it has three problems:
- Links break. A
<section>with a dark background has links that fail contrast unless the editor also remembers to overridetext-primarywithtext-primary-300(or similar). Most editors don't think about link color when they're setting a background. - Heading colors break. Same problem —
<h2 class="text-zinc-900">is correct on light, invisible on dark. - Nothing is reusable. Every section is its own little CSS decision. Twenty pages later, the editor has twenty subtly different versions of "dark section" floating around.
Section presets fix all three at once: pick one preset, and the background + text + links + headings update together. The available presets are defined centrally so changing "the dark preset" everywhere is a one-place edit.
Picking a preset
In the page editor, click the paintbrush button on a row (or section header) to open design mode. The first card in the sidebar is Color Preset with a dropdown showing the current preset and prev/next arrows for quick cycling.
┌────────────────────────────────────────────┐
│ Color Preset ▼ │
├────────────────────────────────────────────┤
│ SECTION COLOR PRESET │
│ ┌────────────────────────────┬─────┬─────┐│
│ │ Dark │ ◀ │ ▶ ││
│ └────────────────────────────┴─────┴─────┘│
│ │
│ LINK COLOR │
│ ┌────────────────────────────────────────┐│
│ │ Default ▼ ││
│ └────────────────────────────────────────┘│
└────────────────────────────────────────────┘
The prev/next arrows cycle through every preset on the system. Useful for "let me try a few" — every click renders the live preview iframe immediately, no save needed.
The second dropdown — Link Color — only appears when the selected preset declares variants. The Dark, Primary, Secondary, and Accent presets each ship with link-color variants (Secondary Links, Accent Links) so an editor on a dark section can flip the link color to whichever brand color reads best against the background. Presets without variants (Light, Light Alt, Primary Tinted, etc.) hide the second dropdown entirely — there's nothing to pick.
A third dropdown — Accent Color — appears when the preset declares accent variants. It controls the row's accent color (the colour used by stats numbers, pricing card checkmarks, feature-grid icons, and any other row element that opts in). It works exactly like the link-color picker but is independent: a row can have any combination of link variant and accent variant. See "Accent color" below.
What a preset controls
Each preset is a small bundle of styling decisions:
| Field | What it controls |
|---|---|
| Background classes | The <section> element's background — Tailwind classes like bg-primary or bg-tone-100 dark:bg-tone-950 |
| Text classes | The default body text color inside the section |
| Link color + Link hover color | What unclassed <a> tags inside the section look like, with separate light- and dark-mode pairs |
| Heading colors (H1–H4) | Optional per-tag overrides — useful when the default text color doesn't read well at large sizes |
| Background image + position/size/repeat | Optional decorative image layered behind the section |
| Variants | The alternate link-color sets the editor can flip to via the second dropdown |
| Accent color (light + dark) | The colour rows use for their accent element — stats numbers, pricing checkmarks, feature icons. Surfaced as --row-accent / --row-accent-dark CSS variables on the section |
| Accent variants | Alternate accent colours the editor can flip to via the third dropdown — independent of the link variant |
Editors edit presets at Dashboard → Design → Sections. The page shows every preset as a live preview card with a colored swatch row showing exactly how text and links will appear. Add new presets, hide unused ones (without deleting), or duplicate-and-customize an existing preset to spin up a new one in seconds.
Why a two-dropdown model
An earlier version of this feature had every link-color combination as its own top-level preset — Dark, Dark Secondary Links, Dark Accent Links, Primary, Primary Secondary Links, Primary Accent Links, and so on. That worked but made the picker noisy: 20 presets, half of them just link-color variations of each other. An editor scanning for "the dark one" had to mentally filter out the dark-with-X-links options.
The current model separates the two decisions:
- Pick the background. Light, dark, primary, secondary, accent, plus tinted/surface/dark variations — 14 top-level choices.
- Pick the link color (optional, only shown when the preset has variants). Defaults to whatever reads best on that background.
The win: a smaller top-level list, with the link-color tweak available as a one-click follow-up when the editor wants it. Switching presets clears any active variant — variants are scoped to their parent — so the editor never ends up with a stale link color that doesn't belong to the new background.
How the styling actually lands
For engineers debugging or extending the system, here's the render path:
Each preset's styling is compiled into a single @utility block in resources/css/presets.css by SectionPresetSyncer:
@utility section-dark {
@apply bg-[#27272a] dark:bg-tone-850 text-tone-100;
& a:not([class]) {
@apply text-primary-300 dark:text-primary-300 underline;
}
& a:not([class]):hover {
@apply text-primary-400 dark:text-primary-400;
}
& h1 { @apply text-...; }
/* etc. */
}
Variants get their own block written after the parent so the cascade naturally favours them when both classes are on the same element:
@utility section-dark--accent-links {
& a:not([class]) {
@apply text-accent-200 dark:text-accent-200 underline;
}
& a:not([class]):hover {
@apply text-accent-100 dark:text-accent-100;
}
}
At render time, <x-dl.section> strips any bg-* / text-* classes already on the section's class field (so the preset wins over whatever the row template baked in), then appends section-{preset} plus — if a variant is active and declared on that preset — section-{preset}--{variant}. The rendered element ends up with both classes; CSS cascade picks the variant's link rules.
The chosen preset id and variant id are stored as content overrides keyed section_preset, section_preset_variant, and section_preset_accent_variant on the row's slug. All three also get baked back into the blade file as field-preset="..." / field-preset-variant="..." / field-preset-accent-variant="..." attrs by BladeFieldSyncer, so a fresh request hits the baked default without a DB round-trip.
The syncer runs every time someone edits a preset in Dashboard → Design → Sections, regenerating presets.css from the current setting in one pass. RebuildAssets then rebuilds the Vite bundle so the new utility class is in the deployed CSS.
Accent color
Link variants only restyle unclassed <a> tags — the :not([class]) selector is the trick that lets the variant rule win without !important. That works for body links but does nothing for elements that already have a color class, like the big numbers in a stats row (text-primary-300) or the checkmark icons in a pricing card (text-primary). To let presets control those without !important hacks or per-row blade edits, the preset emits a pair of CSS custom properties:
@utility section-dark {
--row-accent: var(--color-primary-300);
--row-accent-dark: var(--color-primary-300);
/* …other rules… */
}
@utility section-dark--accent-warm {
--row-accent: #ff8800;
--row-accent-dark: #ffaa44;
}
Rows opt in by writing their accent element's colour as a var(--row-accent, FALLBACK) reference instead of a token:
{{-- Before — hardcoded, not preset-aware --}}
field-classes="text-5xl font-black text-primary-300"
{{-- After — picks up the preset's --row-accent, falls back to primary-300 --}}
field-classes="text-5xl font-black text-[var(--row-accent,var(--color-primary-300))] dark:text-[var(--row-accent-dark,var(--color-primary-300))]"
The fallback preserves the row's original look when no preset/variant is in play (e.g. inserted into a vanilla light page with no section_preset set). When a preset is set, its --row-accent value wins. When an accent variant is also set, the variant's --row-accent overrides the preset's. The whole story is pure CSS cascade — no JavaScript, no !important.
Setting only the light value automatically mirrors it onto --row-accent-dark, so editors who don't care about dark mode get sensible defaults from a single field.
Rows that ship with --row-accent opt-in include: every stats row (content-stats, social-proof-stats, social-proof-stats-counter, and their repeater siblings), the rating rows, the unfeatured-card checkmark icons in every pricing row, and the icon colour in features-grid / features-centered / features-large-icons. Other rows are unaffected — the accent dropdown has no visible effect on them, which is fine.
Sane defaults — accent mirrors link
Out of the box, every preset's accent colour mirrors its link colour, and every link-color variant gets a matching accent variant with the same name and colours. Pick the Dark preset and the stats numbers are already primary-300; flip to the Secondary Links variant and the numbers swap to secondary-200. Editors only have to override when they want a different accent than the link colour — the common case is zero configuration.
The same one-shot backfill applies to existing installs: the first time the preset settings page loads after upgrading, any preset that's missing the accent fields gets them auto-filled from its link colour, persisted, and the CSS rebuilt. Subsequent loads skip the backfill because the keys now exist. Clearing an accent (or deleting an accent variant) and saving is sticky — the empty value persists, the backfill never refills it.
Two independent variant pickers
Link variants live in variants[] on the preset; accent variants live in accent_variants[]. The editor surfaces them as two side-by-side dropdowns when both are declared. A row can mix any link variant with any accent variant — e.g. "Secondary Links + Warm Accent" on a Dark section. Switching the parent preset clears both variants because they're scoped to a specific parent.
Where the wiring lives
- Syncer emission —
SectionPresetSyncer::accentVars()handles the var output; the per-preset block also emits the accent variant utility blocks below the link variant blocks. - Section component class assembly —
section.blade.phpappendssection-{preset}--accent-{vid}when the accent variant key resolves on the preset. - Editor server action —
applyPresetAccentVariant($slug, $variantId)in the page editor stages the override and dispatchescontent-text-resetforsection_preset_accent_variant. - Settings form — the "Accent Color" card in Dashboard → Design → Sections owns the preset's default accent (
accent_color/accent_color_dark) and theaccent_variants[]list. Uses the samepartials.section-variant-color-pickerpartial as link variants.
Edge cases
- Removing a variant from a preset doesn't break pages that still reference it. The variant key is validated at render time against the parent's
variants[]array — an unknown variant id silently no-ops, and the section falls back to the parent preset's default link colors. The same rule applies to accent variants andaccent_variants[]. - Switching a row's preset clears both variants. Variants are scoped to a specific parent —
accent-linkson Dark means "accent links on a dark background" and doesn't make sense on Light. The page editor invalidates both the link-variant and accent-variant overrides automatically when the parent preset changes. - Presets respect dark mode automatically. Every preset declares both light-mode and dark-mode token sets. If the site is in
darkmode (orauto+ the visitor's system prefers dark), the preset'sdark:variants win. - Section background image overrides at runtime. Presets can declare an optional background image that gets inlined as a
style="background-image: url(...)"attribute on the section. This lives outside the static CSS because the URL changes whenever the underlying media item is replaced. - Backgrounds always strip color overlap. If a row template ships with
bg-whitebaked in and the editor picks the Dark preset, the rendered class string strips thebg-whitebefore appendingsection-dark. The preset always wins, regardless of what the row template author originally wrote.
Where the feature lives in the codebase
- Preset data —
section_presetssetting, seeded by the initial migration'sdefaultSectionPresets() - CSS compilation —
SectionPresetSyncerwritesresources/css/presets.css - Render —
resources/views/components/dl/section.blade.php, schema inapp/Support/DlSchemas/Section.php - Editor preset dropdowns —
resources/views/pages/dashboard/pages/⚡editor.blade.php(search forapplyPreset/applyPresetVariant) - Settings page —
resources/views/pages/dashboard/settings/⚡section.blade.php - Persisted attribute names —
BladeFieldSyncer(thefield-preset/field-preset-variantmappings)