Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam ( @bittersweetryan )

Understanding Variable Scope And Variable Injection In Less CSS

By on
Tags:

The Less CSS preprocessor allows you to define variables that can be used throughout your CSS structure; however, as I have been digging into Less CSS more, I have found the rules around variable assignment and variable visibility to be a bit confusing. Sometimes variables can be overridden; sometimes they can't. Sometimes they can bubble-up; sometimes they can't. As such, I thought this would make a valuable exploration.

When you define a variable in Less CSS, the visibility of that variable depends heavily on the context in which the variable was defined.

If you define a variable inside of a CSS ruleset block or CSS guard block, that variable is local to that block. This means that it is hidden from the parent block; but, that it can be seen by the current block and by nested blocks within it (the compiler will search up through nested scopes looking for the variable definition).

If you define a variable inside of a Less CSS mixin, then that variable is injected into the calling context after the mixin has finished executing. This is true, not just for the immediate calling context, but for the entire context stack. That is, of course, until the stack presents a CSS ruleset or CSS guard, which stop the "bubbling." Or, until a parent context defines a variable with the same name - variables cannot be overridden by mixins.

To see this in action, I've created a demo that presents a number of nested mixins. As the compiler runs down through the execution path, it outputs the variable values along the way, showing where variables can and can't be overridden:

// When a variable is defined in a mixin, it is made available in the calling context.
// This includes nested mixin definitions as well as nested mixin invocations.
.test-mixin-scope() {

	#nested.outer() ;

	test-mixin-scope-test: @inner ;

}


// Here, we have several locally-scoped mixins. Each mixin is only available to the
// scope in which it was defined. Each mixin is invoked after it is defined.
#nested {

	// This variable is visible only to the #nested namespace. That said, it will not
	// interfear with the ability for the nested mixins to bubble-up their variables
	// into the calling context.
	@inner: "Namespace default" ;

	.outer() {

		.middle() {

			// Here, we are defining a default value based on the @inner variable that
			// is scoped ot the namespace. Note that this does not intefear with the
			// mixins @inner variable that is locally-defined and injected into the
			// various calling contexts.
			.inner( @input: @inner ) {

				// This value will be made available to all of the calling contexts.
				@inner: "Passed up from das inner mixin" ;

				// Test the translation of the #nested namespace inner to input.
				namespace-test: @input ;

			}

			.inner() ;

			middle-test: @inner ;

		}

		.middle() ;

		outer-test: @inner ;

	}

}


// When we invoke the .test-mixin-scope() mixin, well be triggering the invocation
// of several nested mixin calls; ultimately, this will call the .inner() mixin which
// will create a variable which will become available in the stack up to the body{}.
body {

	// Were going to give the body context its own @inner variable. Since variables
	// act somewhat like "constants", this will prevent the .inner() mixin from
	// overriding the @inner value in parent scopes.
	@inner: "Start the demo" ;

	// Of course, variables arent "exactly" constants. So, you can override the
	// current value inside the context of a single block and/or mixin.
	@inner: "Rock the body" ;

	// Unlike a mixin, a CSS guard (and any other ruleset) creates its own private
	// scope. As such, the @inner value set here will not override the current value
	// of the @inner variable.
	& when ( true ) {

		@inner: "In CSS guard" ;

	}

	.test-mixin-scope() ;

	// Check to see which @inner value is visible.
	body-test: @inner ;

}

When we compile this Less CSS, we get the following CSS output:

body {
	namespace-test: "Namespace default";
	middle-test: "Passed up from das inner mixin";
	outer-test: "Passed up from das inner mixin";
	test-mixin-scope-test: "Passed up from das inner mixin";
	body-test: "Rock the body";
}

There are a number of interesting things to notice about this output. First, the variable injection was not hampered by the fact that the #nested namespace had a local @inner variable definition. This is because the variable injection in influenced by the calling context, not by the defining context. And second, the @inner value, set by the .inner() mixin, was made available to the calling context. But, you can see that the @inner value was not injected into the body-block since it already had a variable named @inner.

Variables, in Less CSS, are powerful constructs; but, they are a bit confusing. At least, they were for me. Even getting this demo to work "as expected" took me a while to put together. Hopefully, this can clear up any confusion for others.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel