Posted in Frontend

Less css for growing projects

When almost all projects are built on top of one or more framework, it’s easy to start thinking of your implementation as a “theme”, rather than something specific to your solution. Before you know it, you have thousands of rows of object-oriented-ish css that in theory could be applied to any codebase and result in a your particular corporate identity “out of the box”. In my experience, this is rarely a good idea, and you will probably end up with tons of unused rules, and little to no traceability.

In large and changing projects, I’ve recently started setting up by creating a unique stylesheet for each view. If you or your solutions isn’t ready for modern things like css-modules yet, this solution might fit you too.

In my case a “view” is any .cshtml that can be rendered individually. This is roughly our block- and pagetypes in Episerver, if you’re in some other environment – adjust accordingly. Pages and blocks are then built locally using references to common utilities (for instance, grid.less, typography.less). Anything that is particular for the view in question gets ruled declared inside a class, that has a name that is traceable to the view.

One of the subtle differences between less and sass is how the compiler treats the @import directive. Where sass will simply mash in the content of the referenced file wherever you put it, less will actually check for previous references and only include the content once (unless explicitly told not to). The same thing can easily be implemented with mixins, if you prefer sass.

When two or more views are very similair, the rules are extracted to a common base class and  each view extends it. DO NOT FALL FOR THE TEMPTATION OF USING THE BASECLASS IN THE VIEW.


/*_content-base.less*/ 
@import "grid.less"; 

.content-base { ... }  

/*news-page.less*/ 
@import "_content-base"; 

.news-page:extend(.content-base all) { ... }  

/*article-page.less*/ 
@import "_content-base"; 

.article-page:extend(.content-base all) { ... }

Because everything is still global, we need to apply a policy regarding our selectors.

Top selectors should always be traceable back to the view it’s containing rules for. Think of these scopes as namespaces*. They inherit up, but never across. No utility classes should be modified inside this scope. The last rule can be bent slightly, if there is a clearcut rule in place, like “all links on article pages are green”. However, be aware that the scopes sometimes get nested (think blocks inside other blocks), so don’t get stuck in a loop of reseting and declaring rules. If in doubt, err on the side of creating modifiers for your utilities. And of course, we still need unique names for all utilities, since they eventually will be squished together into a single stylesheet.

.article-page:extend(.content-base all) {  
  a { 
    color:green; 
  } 
}


In my experience this will encourage maintainable code, and make it easy for new developers to read your stylesheets and clearly see what rules are used where. When you change rules, you’ll have a chance of knowing what exactly on your site is affected. It also let’s you evaluate frameworks like bootstrap, by making clear exactly what parts of it you’re using, and to what extent. Make sure to maintain the ability to render a page with only its needed stylesheets, and keep your stylesheets modular all the way, so that for instance a component in your pattern library can be traced to set of rules in a single file. This sounds like a lot of work, but will save you a lot of time in the long run.

Another upside is that inevitable fuck-ups like 10 levels of nested selectors are still contained into a “namespace”, that makes it possible to refactor and tidy up, without risking loosing a specificity battle somewhere else, that no one thought of.

If you inherited a solution with several thousand rows of css already in place, you can ease in to this way of working by starting with a single blocktype, and extract it’s components to a utils folder. Because less only imports files once, we can isolate the rules for a single blocktype, and still keep all references in the “main” stylesheet until it only contains references to view-specific stylesheets. This creates a slow transitioning, and no need to merge in a complete overhaul of all stylesheets ever in one go.

/*NewsTeaserBlockType.less*/ 
@import "utils/typography.less"; 
@import "utils/headings.less"; 
@import "utils/buttons.less";  

/*Main.less*/  
@import "blocks/NewsTeaserBlockType.less"; 
@import "utils/typography.less"; 
@import "utils/headings.less"; 
@import "utils/buttons.less";  

/*... Thousands of rows of inherited crap goes here*/

Then, when your isolation and refactoring is done, you end up with something like this. Since I’m working in EPiServer right now, all blocks are tied to pagetypes, meaning we only need to import pagetypes and make sure each pagetypes has imports for all of its available blocks.

 /*Transformation complete*/ 
/*Main.less*/ 
@import"pages/ArticlePageType.less"; 
@import"pages/NewsPageType.less";  

/*ArticlePageType.less*/ 
@import "blocks/NewsTeaserBlockType.less";

*Ask one of your backend-developers to explain.

Leave a Reply