Less CSS Won't Import The Same File Twice When Globbing
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:
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:
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:
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 →