Managing Variables: CSS Custom Properties vs. SCSS
Maintaining a consistent design system across a massive game UI requires variables for colors, typography, and spacing. However, standard web development practices for managing these variables can introduce hidden frame-rate penalties and run into specific engine constraints.
This guide explores the mechanical differences between runtime and build-time variables. We will examine the hidden costs of global CSS Custom Properties , explore advanced SCSS techniques for zero-cost design tokens, and establish the correct architectural pattern for safely scoping dynamic variables.
The Reality of Native CSS Variables
Section titled “The Reality of Native CSS Variables”In modern web development, it is common practice to define global design tokens using CSS Custom Properties (variables) attached to the :root pseudo-class. This allows you to update a single variable via JavaScript and instantly change the theme of the entire application.
Here is what a standard web-first implementation looks like:
import Block from '@components/Layout/Block/Block';import TextBlock from '@components/Basic/TextBlock/TextBlock';
const HudPanel = () => ( <Block class="hud-panel"> <TextBlock class="title">Health</TextBlock> </Block>);<body> <div class="hud-panel"> <h1 class="title">Health</h1> </div></body>/* The standard web approach: defining global variables */:root { --primary-color: #ff0000;}
.title { color: var(--primary-color);}Then, updating the variable with JavaScript is as simple as:
import { createSignal, createEffect } from 'solid-js';
const [themeColor, setThemeColor] = createSignal('#ff0000');
createEffect(() => { document.documentElement.style.setProperty('--primary-color', themeColor());});const root = document.documentElement;
function updateThemeColor(newColor) { root.style.setProperty('--primary-color', newColor);}Or by toggling CSS classes that switch between different variable values:
.light-theme { --primary-color: #e62e3a;}While this looks clean, it is highly dangerous in a game environment when updated frequently.
Native CSS variables are resolved at runtime. Because the variable is attached to the :root element, the CSS engine assumes that every single element on the page might rely on it.
Whenever you update a :root variable, you force the engine to traverse the entire Document Object Model (DOM) and recalculate the styles for every single element,
regardless of whether they actually use the variable or not.
The Zero-Cost Alternative: SCSS Preprocessing
Section titled “The Zero-Cost Alternative: SCSS Preprocessing”For static design tokens that do not change during gameplay (such as your game’s core theme, base typography sizes, or margins & padding), you should completely avoid native CSS variables.
Instead, you should use a CSS preprocessor like SASS/SCSS. Because SCSS variables are resolved at build-time , the compiler generates standard CSS with hardcoded values. The layout engine does zero variable resolution math at runtime, costing you zero performance.
At its most basic level, you can define and use an SCSS variable directly within the same stylesheet.
/* Define the SCSS variables locally */$brand-primary: #ff0000;$spacing-large: 2rem;
.hud-panel { padding: $spacing-large;}
.title { color: $brand-primary;}When your UI is built for production, the SCSS compiler strips away the variables entirely. The engine only ever receives and parses the final, hardcoded CSS:
/* The final output the layout engine actually reads */.hud-panel { padding: 2rem;}
.title { color: #ff0000;}Scaling to Global Variables
Section titled “Scaling to Global Variables”While defining a local variable is useful, a massive game UI requires a centralized design system. You need variables that act globally, similar to how the :root selector is used for CSS variables.
To achieve this global reach in SCSS, there are two primary architectural approaches.
Method 1: Manual Imports
Section titled “Method 1: Manual Imports”The most standard approach is to extract all your design tokens into a dedicated file and manually import them into every component stylesheet that needs them.
/* Define your central SCSS variables in one master file */$brand-primary: #ff0000;$spacing-large: 2rem;/* Import the variables specifically for this component */@use "variables" as *;
.hud-panel { padding: $spacing-large;}
.title { color: $brand-primary;}Method 2: Global Variable Injection (Bundlers)
Section titled “Method 2: Global Variable Injection (Bundlers)”A common complaint about manual imports is having to write the @use statement at the top of every single component file across a massive project. If you are using a modern frontend bundler like Vite, you can configure it to automatically inject your global variables into every SCSS file.
Here is how you set up Vite to handle your design tokens globally:
import { defineConfig } from 'vite';
export default defineConfig({ css: { preprocessorOptions: { scss: { // Automatically injects your variables file into every SCSS component additionalData: `@use "@/styles/global-variables.scss" as *;` } } }});Safe Usage: Tightly Scoped CSS Variables
Section titled “Safe Usage: Tightly Scoped CSS Variables”SCSS is perfect for static values, but game UIs still require dynamic theming. For example, imagine a multiplayer scoreboard where each player’s card needs to be dynamically accented with their specific “Team Color” assigned by the server.
For these dynamic features, you cannot use SCSS because the values are decided at runtime. While you could use standard inline JavaScript styles (element.style.color), that becomes incredibly inefficient if the color needs to affect the card’s background, its borders, and its pseudo-elements all at once (since JavaScript cannot target ::before or ::after directly).
Native CSS variables are the perfect tool here, but they must be architected safely. To avoid the global recalculation penalty discussed earlier, you must tightly scope your CSS variables to the specific UI component that needs them.
By applying the variable directly to the component’s container, you tell the layout engine to only recalculate the styles for that specific container and its internal children, rather than the entire document.
Here is how you safely scope a dynamic CSS variable for a Player Card:
<body> <div id="player-1-card" class="player-card"> <div class="card-bg"></div> <div class="card-border"></div> <div class="player-name">Player One</div> </div>
<div id="player-2-card" class="player-card">...</div></body>/* 1. We set a safe default directly on the component, not the root */.player-card { --player-accent: #cccccc; position: relative;}
/* 2. Multiple children inherit the same dynamic variable */.card-bg { background-color: var(--player-accent); opacity: 0.2;}
.card-border { border-width: 2px; border-style: solid; border-color: var(--player-accent);}
/* 3. The variable works on pseudo-elements, which JS cannot easily reach! */.player-card::after { content: ""; position: absolute; box-shadow: 0 0 15px var(--player-accent);}const playerCard = document.getElementById('player-1-card');
// When the engine assigns the team color, we update the specific nodefunction updatePlayerColor(hexColor) { // This instantly updates the bg, border, and pseudo-element glow! playerCard.style.setProperty('--player-accent', hexColor);}© 2026 Coherent Labs. All rights reserved.