Here's a limitation most CSS frameworks share and rarely say out loud: if you load them from a CDN, you can't really change the colors. The palette was baked in at build time. To make it match your brand you have to install the toolchain, edit a config, and recompile. Farvist's v1.3 release was about removing that constraint entirely. This is how it works.
The problem with baked colors
A framework built in Sass typically computes derived shades at compile time. A button's hover state is darken($primary, 8%); a soft badge is rgba($primary, 0.16); a neon glow is a shadow tuned to the brand. All of that math runs once, during the build, and freezes into hex values in the shipped CSS. Change $primary and you have to run the build again to propagate it.
That's fine if you have a build. But a CDN user โ or an AI assistant writing CSS in a sandbox โ doesn't. For them, the palette is effectively read-only.
Step one: mirror every token to a custom property
The first move is to stop consuming Sass variables directly in component rules and instead consume CSS custom properties that are emitted from those Sass variables. The Sass tokens become configuration; the custom properties become the live wiring.
/* one place emits the tokens */
:root { --fv-primary: #6d4af5; --fv-info: #22d3ee; /* โฆ */ }
/* components read the variable, never the Sass value */
.text-primary { color: var(--fv-primary); }
Now overriding --fv-primary on :root recolors anything that references it โ instantly, at runtime, with no rebuild. That alone handles flat colors. The hard part is the derived ones.
Step two: derive shades with color-mix() instead of Sass
This is the key that makes the whole thing work. color-mix() does at runtime what Sass color functions do at build time โ and it accepts custom properties as inputs. The translations are close to one-to-one:
/* build-time Sass โ runtime CSS */
rgba($primary, 0.16) โ color-mix(in srgb, var(--fv-primary) 16%, transparent)
color.mix($white, $c, 45%) โ color-mix(in srgb, white 45%, var(--fv-primary))
color.scale($c, -10%) โ color-mix(in srgb, var(--fv-primary) 90%, black)
Because rgba(C, 0.16) is mathematically the same as mixing 16% of C with transparent, the default output is pixel-identical to the old baked version โ we verified that by diffing computed styles across every component against the previous build. But now the inputs are variables, so the same declarations follow a re-brand. Gradients, the frosted-glass mesh backdrops, the body's ambient orbs, hover and active states, focus rings โ all of it recolors from one override.
The escape hatch: contrast
One thing genuinely doesn't survive the move to runtime: WCAG contrast math. Whether white or black text is readable on a fill depends on the fill, and there's no pure-CSS way to compute "the accessible text color for this variable." So each color exposes an optional companion:
:root {
--fv-primary: #f59e0b; /* a light amber brand */
--fv-primary-contrast: #1c1917; /* dark text on the fill */
}
The default is computed at build time for the default palette; you only override the contrast token when your brand flips between a light and dark fill. It's the one place the human (or the AI) has to make a judgment call, so it's explicit rather than magic.
Guarding it in CI
A refactor like this has a sneaky failure mode: someone later writes color: #6d4af5 directly in a component, and it silently stops following the theme. So there's a CI gate that greps the compiled CSS and fails the build if any brand color appears outside a --fv-* definition. Runtime theming can't quietly regress, because "quietly" is exactly what the gate is watching for.
What it buys
Re-branding the entire framework is now five lines of CSS you can paste anywhere โ including into a CDN-only page, or into a prompt. Prebuilt skins like data-theme="synthwave" are nothing more than small blocks of these same token overrides, each checked for contrast in CI. And the thing that made all of it possible โ color-mix() โ is already in every browser Farvist supports, so there was no baseline cost to pay.
The broader lesson: the line between "configuration" and "runtime state" is worth moving as far toward runtime as the platform allows. Every token you can resolve in the browser is a token your users can change without your toolchain. That turns out to matter more than ever, now that some of those users are models.
You can try it live on the homepage โ click a brand and watch the page recolor โ or read the theming docs.