CSS preprocessing in Magento 2

Magento 2 CSS preprocessing v1

Magento 2 has been out in the wild for some time now. Most developers I know haven’t worked on Magento 2 project yet. However, most of them installed it and played around with it, learning and experimenting.

From the comments on github and social media, and from chats with developers, conclusion is that Magento 2 Frontend has a lot of issues, one being the most important: CSS preprocessing.

Magento 1

From standpoint of a frontend developer that most of the time works on custom projects, Magento 1 CSS was a piece of cake. You could just include your CSS files through XML, for example like this, in page.xml file:

<default translate="label" module="page">
	<block type="page/html" …… template="page/3columns.phtml">
		<block type="page/html_head" name="head" as="head">
			<action method="addCss"><stylesheet>css/styles.css</stylesheet></action>
      		</block>
	</block>
</default>

And you could use ( or not ) whatever you want to generate that file. Madison Island theme, that first came out with Magento 1.9, is using Sass preprocessor for compiling CSS.

How does it work in Magento 2?

In Magento 2, you still add your CSS files through XML. It looks a bit different now, but that is a whole different story 🙂

<page>
	<head>
        	<css src="css/styles-m.css" />
        	<css src="css/styles-l.css" media="screen and (min-width: 768px)"/>
        	<css src="css/print.css" media="print" />
    	</head>
</page>

If you view source of default Luma theme homepage, you can see that main CSS files are located in /pub/static/frontend/Magento/luma/en_US/css/styles-l.css . Knowing that CSS and LESS files are actually stored in Theme directory, how did files end up there?

Request flow

DIAGRAM

As you can see from the diagram, the system first searches for the file in pub/static directory. If file is present there, it will just return that file.

If file is not in that folder, system will search for it in your active theme. If it founds it there, it will create a file in pub/static/… with a symlink to the actual file located in your theme.

If system does not find the file in your theme either, it will search for the same file, but with .less extension, and then the whole LESS preprocessing is put in motion, which we’ll cover later.

First step of this flow practically means that we could work in Magento 2 the same way we did in Magento 1: add css file via XML, the system will search for it either in pub/static or our theme, and if it finds it it will just use that file. No LESS preprocessing is triggered. You could hypothetically use whatever you want to generate that CSS file…

But, what about…?

But, in that case, you are coding against Magento 2 best practices. Extensions and themes will not work together “out of the box”.
If you want to follow the rules, and keep your theme modular and compatible with extensions on the market, you will use built-in LESS preprocessor.

Less preprocessing

LESS is preprocessor, which extends the capabilities of CSS, adding features like variables, functions, mixins or nesting, turning it into something similar to a primitive programming language. Developers write their code in LESS syntax, which is in the end compiled to CSS.
For Magento, it was important to make a system where extensions or themes would work out of the box. Where a merchant can install extension or theme on his own, and it would just work. So they had to figure out a way to let extensions communicate with the themes, making style inheritance or overrides simple. That’s where preprocessing steps in. For example, if your theme uses variable for h1 color:

@heading-color: #00FF00;

and in the .less file:

h1 {
    color: @heading-color;
}

Which is compiled to this:

h1 {
    color: #00FF00;
}

You could write an extension which can use the same color, without even knowing what that color is.

.my-heading {
    color: @heading-color;
}

Compiled code will look like this:

.my-heading {
    color: #00FF00;
}

As you can see, extension developer can use the same color, even when he doesn’t actually know which color is that.

OK, now you know why Magento implemented preprocessor, but you may be wondering why did they choose LESS, when you know that most of frontend developers use more powerful Sass preprocessor? There is no big mystery here: at the time of decision making, LESS was the only preprocessor with stable PHP compiler.

Frontend development workflow

In Magento 2, there are two modes of frontend development workflow available: Client side and server side. This is configurable in Magento administration under: Stores > Configuration > Advanced > Developer > Frontend development workflow

Client side – compilation happens inside the browser via native less.js compiler.

Server-side is default workflow and the only option available in production mode. It uses Magento built-in PHP compiler.

If you are serious into Magento development, you won’t be using client-side preprocessing. But you probably aren’t going to use server side PHP compiler either. We’ll talk about the reasons little later.

So how does LESS compilation actually work? We already discussed CSS file request flow.

  1. System searches for requested .css file. If found, preprocessor stops execution.
  2. System searches for the same file, with .less extension, following theme fallback mechanism. If file is not found, preprocessor stops execution
  3. Reads the contents of .less files and resolves @magento_import and default @import directives
  4. Resolves all paths in .less files to relative paths in the system using theme fallback mechanism. All resolved paths are stored to var/view_preprocessed/less directory
  5. All source files are passed to PHP compiler and resulting .css files are published to pub/static/frontend/<Vendor>/<theme>/<locale>

@magento_import

Default LESS @import imports contents of specified file into current file. It allows developers to break down their styles into smaller logical files:

@import "modules/global.less";
@import "modules/header.less";
@import "modules/search.less";
@import "modules/forms.less";
@import "modules/buttons.less";

Magento had a problem here, since standard LESS @import directive isn’t aware of fallback mechanism and isn’t aware which modules are installed and how to add their own .less files.

They solved this by adding //@magento_import directive, which allows including multiple files with the same name. This allows extension developers to “plug in” their styles via module.less file.

//@magento_import 'source/_module.less';

To avoid conflicts with default LESS, @magento_import must be written as a comment – with two slashes.

This is what I call Magento pre-preprocessor, or how Sandro poetically called it, Pre2processor.

So, when compiler hits @mageno_import, in our example, it will look for all source/_module.less files in our theme, following fallback rules. And it will replace @magento_import with default LESS @import directives:

@import '../Magento_AdvancedCheckout/css/source/_module.less';
@import '../Magento_Bundle/css/source/_module.less';
@import '../Magento_Catalog/css/source/_module.less';
@import '../Magento_CatalogEvent/css/source/_module.less';
@import '../Magento_CatalogSearch/css/source/_module.less';
….

This files, with resolved paths are published to var/view_preprocessed directory, and then passed to standard LESS compiler.

Cleaning, reloading, waiting, waiting…

As you could see, when request for .css file is made, system will search it in pub/static folder. If it is there, it will just serve that file. That means, if you make any changes to you .less or .css file in your theme, the system will not be aware of it. You will have to delete the file in pub/static/ folder so that system picks up the changes in .css files in your theme.

Also, Pre2processor has generated files with resolved paths in var/view_preprocessed folder, so if you make any changes in .less files, you will have to clean that folder also.

That is a lot of cleaning… Also, compiling takes forever, so if you make changes in your css and want to see the result in the browser, it will take a lot of patience. If you know what I mean…

Automated Preprocessing with Grunt

When it became obvious that this workflow will be too slow and painful for frontend devs, Magento decided to add automation (and speed) to this process, by adding the support for Grunt task runner.

Grunt is a javascript task runner, which helps you with the automation of repetitive tasks. In our case, those would be: watch, compile, clean…

Magento 2 comes with built in grunt tasks, but there are some steps you need to take before you can use it.

The problem with Grunt is that we are using node.js for compilation, and it is not aware of theme fallback mechanism and doesn’t know what @magento_import is…. Luckily, there is a Magento CL directive:

bin/magento dev:source-theme:deploy

… which essentially does this:

  1. Resolves all fallback paths
  2. Creates symlinks to source (.less) files
  3. Expand all “@magento_import” to import single files
  4. Publish all files in a tree to pub/static folder

So, now you have the whole tree in pub/static folder, and you can watch for changes and compile those files using Grunt tasks.

There are several tasks available:

  • grunt clean:<theme>
    • Removes static files from pub and var folders
  • grunt exec:<theme>
    • Creates whole tree with symlinks to the source .less files in pub/static/frontend/vendor/theme/web
  • grunt less:<theme>
    • Compiles .css files using symlinks published in pub folder
  • grunt watch
    • Watches for changes in source .less files, compiles .css and injects new styles in browser without page refresh (via Livereload for example)

Please note that most of these tasks are just a wrappers for bin/magento directives.

There are few quirks with this approach. As already stated, node.js isn’t aware of magento flavored LESS, so in case you make changes to root LESS files ( styles-l.less ), or add, remove or rename imported .less files, you will have to rebuild the whole tree again. This means -> stop the watcher -> grunt exec -> grunt watch.

Compiling with grunt takes aprox. 8-10 seconds, depending on the size of your .less files, which is a great progress compared to PHP compiler.

Less is no(t) more

So, this is a roundup how CSS preprocessing works in Magento 2. As you could see the process is pretty complicated and not so efficient. And while it is fairly modern, it is using some technologies that are already “passe”. Most of frontend devs these days are using Sass in combination with Gulp. This is the reason why community engaged in some interesting projects:

Gulp implementation:

https://github.com/poddubny/magento2-gulp

https://github.com/SnowdogApps/magento2-frontools

I’ve tested Gulp combination, and compile with Gulp takes around 3-4 sec, which is again great time saver compared to PHP and Grunt compiler.

blank Sass theme:

https://github.com/SnowdogApps/magento2-theme-blank-sass

If you hear the voices from community, most of them are asking for Sass+Gulp combination. While this is understandable, I still preach “platform” agnostic frontend. Complete separation of frontend, and allowing developers to use whatever tools and pre/post/no processor they want. Frontend tools and techniques are a moving target and are changing dramatically almost daily. Who knows what will the future bring.

With CSS variables landing in all major browsers ( minus IE atm ), and with Chrome engineers working on new @apply  CSS rule , I believe we will all write our code in vanilla CSS in the years to come.

To wrap it up…the good thing is that community is talking about this, and Magento is listening… Let’s hope we will work out the best solution for all of us.

 


About Hrvoje Jurisic

Team Leader, Frontend Developer, Designer / Illustrator

Hrvoje is Certified Frontend Magento Developer and creator of Inchooers, your favorite comic.

Read more posts by Hrvoje / Visit Hrvoje's profile

12 comments

  1. I have updated to Magento 2.1.1

    I have crated a new theme and added _theme.less. Changes in _theme.less are reflecting but when i am trying to import custom less file in _theme.less, grunt is not compiling it.

    Any idea how to resolve it.

    1. try with “_” prior name like “_example.less” instead of “example.less”

  2. Apprecaite ! very helping tutorial for the newbie of Magento2. Hopefully waiting for the Sass enabled Mage Core.

    Thank you

  3. For now untill scss will not be inside the core of magento 2 – I this the way approach is to use gulp with the less default files who comes with blank theme .

    What is your opinion about it?

  4. So here’s my problem. I have my own theme, with Blank as the parent theme, and in my theme dir I have two .less files: file1.less and file2.less.
    When I do grunt exec for this theme, the \pub\static\frontend\\\en_US\css\source will be filled with symlinks to a lot of .less files, including the ones I mentioned above.
    However, if a Less file contains an @import, it will not create a symlink upon grunt exec, but a COPY of the file — regardless if it’s the standard @import method or the //@magento_import version. Hence, editing the original file will not change the one in the pub/static dir.

    Any idea why this happens?

    Second question: what is the normal/recommended workflow when you’re working on a theme? As in, when/how often/in which order do you have to do grunt exec, grunt less, and static content deploy? Note: I’m on Windows, and when I do a static content deploy, it creates copies of the .less files in pub/static, but when I do grunt exec, it creates symlinks.

    For that reason, I usually try to avoid static content deploy.

    Thanks so much!

  5. Thanks for this, however I am having issues getting Livereload to work. I have the livereload extension installed in my browser (Chrome), have enabled it, and am running “grunt watch” in my Magento installation, which runs without errors. However I am expecting my page to refresh automatically whenever I upload a file – currently this isn’t the case. Anything I’ve missed?

  6. Thanks for the guide, though I just followed the above and ran “grunt exec:mytheme” on the root of my Magento install, and received “gruntfile not found…..”. Any ideas?

    1. Look in your Mag2 root, there will be a Gruntfile.js.sample there – remove the .sample part and you’re good to go 🙂

  7. Great work..!! I would really appreciate it if you could tell me about the integration of wordpress and magento..is there any plugin or tool available out there??

    I would be really greatful to you..Please.!!

    Thanks,
    Ankita Garg

  8. Thanks for the explanations, I just started a client project with Magento 2 and was very surprised how complicated the whole frontend setup is… It took me quite some time to understand how everything is connected and I doubted myself if I’m to stupid to understand it. And I used to work with GRUNT/SASS for some time now… Anyhow, thanks for the resources at the end of the article. I wasn’t aware of that.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.