Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Aaron Lerch
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Aaron Lerch

Less CSS Won't Import The Same File Twice When Globbing

By
Published in

When organizing my Less CSS files in a multi-page web application (MPA), I often have a folder full of modules in which each .less file represents a unique component. For the most part, the order in which these modules are imported is irrelevant since they represent isolated definitions. The exception to this rule is the theming and design system modules. In order to work with the CSS cascade, it's important that these design system modules be imported first such that other modules can override properties locally without having to worry about CSS selector specificity. Thankfully, Less CSS makes this easy with its automatic (default) deduplication of import paths.

For this exploration, I am using this package.json file:

{
	"scripts": {
		"build": "lessc --glob ./src/main.less ./dist/main.css"
	},
	"devDependencies": {
		"less": "4.2.0",
		"less-plugin-glob": "3.0.0"
	}
}

The less package is the Less CSS compiler; and the less-plugin-glob package allows me to use * and ** in my @import paths.

Now, consider my modules folder with the following .less files (I'm including the content of each file in a single snippet here in order to reduce the noise):

/* file: ./modules/a.less */

.a::before {
	content: "a" ;
}

/* file: ./modules/b.less */

.b::before {
	content: "b" ;
}

/* file: ./modules/c.less */

.c::before {
	content: "c" ;
}

/* file: ./modules/design-system.less */

.design-system::before {
	content: "Design System" ;
}

If my main .less file looked like this:

@import "./modules/*.less" ;

... then, compiling the CSS file will import each .less file into my main.css. The less files will be imported in lexicographic order (ie, alphabetically) by file name. Which results in the following CSS output:

.a {
  content: "a";
}
.b {
  content: "b";
}
.c {
  content: "c";
}
.design-system {
  content: "Design System";
}

As you can see, each file was imported in lexicographic order by file name. And, unfortunately, this puts our design system content at the very end, which is problematic from an overrides perspective. Consider this HTML:

<h1>
	Testing CSS Cascade
</h1>

<p class="design-system c"></p>

The intent here is use the base styles from .design-system and then override some of those styles with .c. However, due to the CSS cascade rules, rendering this HTML page results in the following output:

Rendering of CSS rules shows that they are applied in order of definition if specificity is the same.

As you can see, .c failed to override the .design-system because the .design-system was defined last in the CSS file (and has the same specificity).

To fix this, we can update the main .less file to specifically import high-priority Less CSS files first, before executing our globbing import:

// In order to work with the cascade, we need to include our design system definitions
// first. This way, all other modules can consume and then override properties of the
// design system, even if the selector specificities are the same (last one wins).
@import "./modules/design-system.less" ;

// NOTE: The design-system will NOT BE included twice.
@import "./modules/*.less" ;

As you can see, both @import statements reference the ./modules/ folder. But, we're explicitly importing the design-system.less file first before globbing the rest of the ./modules/ folder. And, when we compile the CSS file this time, we get the following output:

.design-system::before {
  content: "Design System";
}
.a::before {
  content: "a";
}
.b::before {
  content: "b";
}
.c::before {
  content: "c";
}

This time, the design-system.less file content was placed at the top of the compiled CSS output due to our explicit @import. And, most importantly, it wasn't included a second time as part of the *.less globbing. This is because the Less CSS compiler won't import the same file twice (the default behavior).

Now, if we go to render the previous HTML page, we get the following output:

Rendering of CSS rules shows that they are applied in order of definition if specificity is the same.

This time, since the design-system.less file was included first, out c.less file definition is able to override the content property.

This is a rather helpful behavior when the execution / import order of a small number of .less files is important. It means that we can be explicit about importing a few files first and then just brute-force globbing the rest of the files without having to worry about duplication. This keeps life simple.

Epilogue on CSS Layers

Recent releases of modern browsers are now implementing CSS layers. CSS layers gives us more control over the cascade specificity, regardless of the order in which CSS properties are declared. I've never actually looked at this before; but, let's hack something together as a proof-of-concept.

In our design-system.less file, let's wrap the CSS property block in an @layer called ds:

@layer ds {

	.design-system::before {
		content: "Design System" ;
	}

}

Now, in our main.less file, instead of explicitly including this design-system.less file first, we can just declare the ds layer first:

@layer ds ;

@import "./modules/*.less" ;

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

@layer ds;

.a::before {
  content: "a";
}
.b::before {
  content: "b";
}
.c::before {
  content: "c";
}

@layer ds {
  .design-system::before {
    content: "Design System";
  }
}

This tells the browser to push the ds layer onto the layer stack immediately. Then, when the subsequent design-system.less block is executed, it will push styles onto the already-defined ds layer. This puts the ds layer above the "anonymous" layer—that is, the block of unlayered CSS in .a, .b, and .c—even though the .design-system block is still defined last.

And, when we run the above HTML with the layered CSS, we get the following output:

CSS layer rules give us control over the specificity of CSS.

As you can see, even though our design-system.less file was the last to be imported, the .c block was still able to override the content property because it was declared in an anonymous layer higher up in the layer stack.

This native layering stuff is pretty cool! Definitely something worth looking into.

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

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

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