Variance: Modifiers, Cascades & State

In vBEM, you change a component’s appearance purely by reassigning CSS variables. This document shows you how to handle hover states, global layout changes, and dynamic JS data without writing messy CSS overrides or passing React props down 5 levels.

1. Adapting to the Parent Layout (BEM Mixing)

When a standalone component needs to look different because of where it is placed (e.g., a generic button placed inside a dark header), do not write deep CSS overrides like .header .button { ... } or pass React props like <Button variant="header" />.

Instead, use BEM Mixing. Apply both the base component class and a contextual mixin class to the same HTML element.

The HTML:

<header class="header">
	<!-- The element is a button, but it receives contextual variables from the header -->
	<button class="button header__button">Log In</button>
</header>

The vBEM Blocks:

// global
:root {
	--color-blue: #3b82f6;
	--color-white: #ffffff;
}

// 1. The Base Component
.button {
	--bg-color: var(--color-blue);
	--text-color: var(--color-white);

	background-color: var(--bg-color);
	color: var(--text-color);
}

// 2. The Contextual Parent
.header {
	// Static value, does not variate
	background-color: #000000;

	// BEM Mixing: Mutates the button's contract securely.
	// This keeps our CSS perfectly flat without specificity wars.
	&__button {
		--bg-color: transparent;
		--text-color: var(--color-white);
	}
}

1.1 A Quick Note on Source Ordering and Components

Because vBEM resolves variance through flat CSS specificity, the final source-order of your stylesheets dictates how classes are applied. For example, if the .button styles are defined after the .header__button mixin, the properties of .button will take precedence over the overrides, as the overrides were defined earlier in the source ordering.

At scale, vBEM leverages structured component architecture, meaning that BEM mixing generally only modifies primitive (Core) components instead of other more complicated ones (Composite components, which are representative of complex arrangements of Core components, often serving a direct business case). Ensuring that your Core Components (like .button or .input) are imported before your Composite Components (like .header or .data-grid) is essential for consistent behaviour as the application scales.

2. Parent-Driven State (Updating Multiple Elements)

Instead of passing an isError boolean to a Label, an Input, and a Message component separately, you should apply a single state modifier to their parent wrapper. By changing the parent’s variable contract, all internal elements update automatically.

// global
:root {
	--color-border-base: #cbd5e1;
	--color-danger: #ef4444;
}

// components
.form-group {
	// 1. VARIANCE PROPERTIES (The Contract)
	--accent-color: #64748b;
	--border-color: var(--color-border-base);
	--message-display: none;

	// 4. ELEMENTS, DELEGATION & MODIFIERS
	&__label {
		color: var(--accent-color);
	}

	&__input {
		border-color: var(--border-color);
	}

	&__message {
		display: var(--message-display);
		color: var(--accent-color);
	}

	// 💥 Parent-Driven Update:
	// This single parent modifier makes the label red, the border red, and shows the message.
	&--error {
		--accent-color: var(--color-danger);
		--border-color: var(--color-danger);
		--message-display: block;
	}
}

3. Hierarchical Elements (Passing State to Children)

When a parent element is hovered or active, do not write chained CSS targeting the child element (e.g., .sidebar__item:hover .sidebar__item__link { ... }).

Instead, use Hierarchical Elements. The parent updates a local variable when hovered, and the child pseudo-element simply consumes that variable.

// global
:root {
	--color-primary: #e2e8f0;
	--color-interactive: #3b82f6;
	--color-text-base: #334155;
}

// components
.sidebar__item {
	// 1. VARIANCE PROPERTIES (The Contract)
	--item-accent: var(--color-primary);
	--item-color: var(--color-text-base);
	--accent-width: 3px;

	// 4. ELEMENTS & MODIFIERS
	&:hover {
		--item-accent: var(--color-interactive);
		--accent-width: 5px;
	}

	// 💥 The Variable Route
	// The child naturally inherits the mutated variables without deep CSS targeting.
	&__link {
		// NOTE: That instead of simply cascading the color, we apply the attribute to
		// exactly to where it's needed. We can delineate other colors as needed.
		color: var(--item-color);

		&::after {
			width: var(--accent-width);
			background-color: var(--item-accent);
		}
	}
}

4. Dynamic JS Data (The “Root Prop”)

Modern applications often deal with dynamic API data (e.g., user-defined brand hex codes, scroll positions, or dynamic heights).

Do not use React/Vue inline styles for raw CSS properties. Instead, pass dynamic data as Root Props (inline CSS variables) on the component’s root HTML element. The vBEM SCSS will intercept it and route it safely.

The HTML / JSX:

// Pass the API data exclusively into the defined Root Prop
<div class="profile-card" style={{ "--user-brand-color": user.themeHex }}>
	<button class="button">Follow</button>
</div>

The vBEM Block:

// global
:root {
	--color-blue: #3b82f6;
}

// components
.profile-card {
	// Intercept the inline Root Prop. Always provide a system token fallback.
	--brand-color: var(--user-brand-color, var(--color-blue));

	& > .button {
		// Map the JS data directly into the nested button's API
		--bg-color: var(--brand-color);
	}
}

5. Global App State & Mathematical Layouts

Passing a boolean prop (like isMobileMenuOpen) down through 5 levels of the DOM to hide a layout column is an anti-pattern. Instead, add a Root Flag (a class on the <html> or <body> tag) and let the vBEM component intercept it locally. Because we are only mutating known attributes within a targeted component scope, we limit unwanted side effects.

When you need to collapse layout space, do not use display: none. Use a Mathematical Toggle (a variable acting as a 1 or 0 multiplier) to securely collapse physical space.

// global
:root {
	--sidebar-size: 280px;
}

// components
.layout {
	// 1. VARIANCE PROPERTIES (The Contract)
	--sidebar-scale: 1;

	// 3. PROPERTY SET (The Execution)
	// Multiply the size by the scale. If scale is 1, it's 280px. If 0, it's 0px.
	--sidebar-real-size: calc(var(--sidebar-scale) * var(--sidebar-size));
	grid-template-columns: var(--sidebar-real-size) 1fr;

	// 4. ELEMENTS & MODIFIERS
	// Watch the global app state and safely collapse the layout math.
	:root.mobile-hidden & {
		--sidebar-scale: 0;

		// Example edge case fix. For accessibility, we can modify the component
		// state by detecting active states of it's children. In this case, because
		// the sidebar is progressively enhanced, we opt to show the sidebar
		// if reader or keyboard indexes over it.
		&:has(.sidebar:focus-within) {
			--sidebar-scale: 1;
		}
	}

	@media screen and (max-width: 800px) {
		// Automatically collapses on mobile devices
		--sidebar-scale: 0;
	}
}

6. Overlapping States (Order of Precedence)

What happens when a button has a primary color, but it is currently hovered, and also disabled?

Because vBEM maintains flat CSS specificity (0,1,0 for pseudo-classes, 0,2,0 for modifiers), the state that wins is determined entirely by top-to-bottom ordering in Part 4.

Always define your states in the following order: Base Modifiers $\rightarrow$ Interaction States $\rightarrow$ Override States.

// global
:root {
	--color-blue: #3b82f6;
	--color-blue-hover: #2563eb;
	--color-grey-light: #f1f5f9;
}

// components
.button {
	// 1. VARIANCE PROPERTIES
	--bg-color: var(--color-blue);
	--cursor-state: pointer;

	background-color: var(--bg-color);
	cursor: var(--cursor-state);

	// 4. ELEMENTS, DELEGATION & MODIFIERS

	// A. Base Variants
	&--primary {
		--bg-color: var(--color-blue);
	}

	// B. Interaction States
	// Put after base variants so hover works on both default and primary buttons.
	&:hover {
		--bg-color: var(--color-blue-hover);
	}

	// C. Override States
	// Put at the absolute bottom. If a button is loading, hover states are ignored.
	&--is-loading {
		--bg-color: var(--color-grey-light);
		--cursor-state: wait;
	}
}