A Look at CSS and Less in Magento 2
Magento 2 leverages the enormous power of CSS pre-processing (via Less) to make theme customization intuitive and easily maintainable through features like variables, mixins, and imports. Less compilation is a fundamental part of M2’s static content deployment and development tools, streamlining the theming process and ensuring that frontend developers can focus on their code, not compiling their final CSS.
The conventions and structures the native Less implementation use are smartly architected and designed to allow flexible and unobtrusive customizations. But along with the power comes the simple fact that there are many different ways to skin a given problem, and some techniques are better than others. If you spend a great deal of your time theming Magento sites, you’ll definitely want to have a broad understanding of the core conventions and, by extension, the best practices we can intuit from them. We’ll be covering both in this article.
I’ll presume that you have a basic familiarity with the details on theming, Less and dev tools available in Magento’s Frontend Developer Guide, as well as core Less concepts like variables and mixins.
Bird’s-eye Overview
Let’s hit the broad highlights of how Magento natively structures Less.
There are three major locations styles are defined:
view/{area}/web/css/source
in an extension’s root directory- e.g.,
/app/code/MyCoolVendor/MyCoolApp/view/frontend/web/css/source/_module.less
- Note that you won’t see this in the core packages, because baseline styles for these are actually contained in the Magento/blank theme, as referenced below.
- e.g.,
/lib/web/css/source
, and in particular,/lib/web/css/source/lib
- e.g.,
/lib/web/css/source/lib/_buttons.less
- These constitute Magento’s UI library styles. No direct style implementations are present here, but rather reusable components (mostly mixins) intended to be utilized in a theme.
- e.g.,
- In a theme, in two major contexts:
- Relative to a specific extension
- e.g.,
/app/design/frontend/MyCoolVendor/my-cool-theme/Magento_Catalog/web/css/source/_extend.less
- e.g.,
- More generic files directly in
web/css
- e.g.,
/app/design/frontend/MyCoolVendor/my-cool-theme/web/css/source/_theme.less
- e.g.,
- Relative to a specific extension
The bread and butter of the native theme implementation is in the extension-specific files.
As a final note, the following is focused primarily on the frontend structure and presumes a theme that extends Magento/blank.
Manifest Files
While the lion’s share of theme styles are contained within extension-specific files, before those are imported we have a foundation composed of broader styles imported by manifest files (importing other manifest files importing other manifest files). There’s really nothing to these but successive lists of imports.
Let’s look at the primary starting manifest file, web/css/_styles.less
relative to the theme root:
@import 'source/lib/_lib.less';
@import 'source/_sources.less';
@import 'source/_components.less';
As you can see, this vanilla version from Magento/blank imports three other manifests. _lib.less
will import the UI library mixins and their associated variables, _sources.less
will import core files that use those library mixins to implement certain baseline theme styles, and _components.less
will import styles specific to interactive components like (in fact, only) modals.
The Main Compiled Files
Zooming our perspective out further, we find the outermost, or top-level, Less files that correspond directly with the final compiled CSS. styles-m.less
and styles-l.less
are the main event here, though there are email- and print-related top-level files as well. Importing _styles.less
is nearly the first thing done here, but there’s a lot more to unpack.
It’s easy to see the distinction between styles-m
and styles-l
by observing the way they’re included in layout:
<css src="css/styles-m.css"/>
<css src="css/styles-l.css" media="screen and (min-width: 768px)"/>
styles-m
contains universal and small screen styles, while styles-l
is restricted by a media query making it specific to large screens. Here are the condensed contents of the Magento/blank version of styles-m.less
:
@import 'source/_reset.less';
@import '_styles.less';
@import (reference) 'source/_extends.less';
//@magento_import 'source/_module.less';
//@magento_import 'source/_widgets.less';
@import 'source/lib/_responsive.less';
@media-target: 'mobile';
@import 'source/_theme.less';
//@magento_import 'source/_extend.less';
styles-l.less
has a few notable differences, but it looks much the same. The most important distinction looks like this:
@media-target: 'desktop';
@media-common: false;
The different values for these two Less variables are key in determining the output of the two files.
- Universal styles that should apply at any screen size are wrapped in the CSS guard
& when (@media-common = true) {
and thus only output instyles-m
. (The default value of@media-common
is true.) - Breakpoint-specific styles are defined using a mixin definition like
.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) {
. The imported filesource/lib/_responsive.less
outputs the results of such mixins within typical CSS media queries, using the same two variables in its conditionals, thus allocating only the approprite styles to each final file.
The above should make it clear why it’s important not to include “naked” styles in your Less files, but rather always wrap them in a media-width
mixin or the @media-common
guard. Otherwise, your styles will be duplicated between both final CSS files.
The final structure of interest in the main compiled files are the references to @magento_import
, which you’ll notice are actually preceded by Less comment characters. This directive is used by Magento in its pre-processing and transformed into a succession of actual Less imports. _module.less
, _widgets.less
and _extend.less
are not expected to be present in only one location relative to the theme; many such files may exist relative to specific extensions (whether present in the original extension package or in the appropriate sub-directory in the theme). During pre-processing, this directive is exploded into multiple normal import statements – one for each found path. This constitutes an important difference for how these filemames are treated and how you, in turn, should use them.
Important Named Files
We’ve looked at the general flow of how Magento compiles its final CSS files. Before we delve into some practical guidelines, let’s see a run-down of the most common Less files you’re going to use to implement your styles.
_module.less
: The core styles for a specific extension. Imported from multiple locations via@magento_import
._extend.less
: Additional theme-specific styles for a specific extension, or for the theme as a whole. Imported from multiple locations via@magento_import
._extends.less
: Styles to be used by extend selectors. Imported by reference from the genericweb/css
location._theme.less
: Theme-specific variable overrides. Imported from the genericweb/css
location.
As we won’t really be covering it later, _extends.less
is worth touching on, and it’s worth noting that it’s included here not because you’re really likely to use it frequently, but because the name similarity with _extend.less
demands some attention, lest you confuse the two. _extends.less
is imported with the “(reference)” directive, meaning Less will import it for use by other files but not actually output the contents. It’s intended specifically for the definition of common style blocks that are to be used with the Less :extend
selector to implement them easily in various contexts throughout the theme. (As an example, the Magento/blank version implements styles for .abs-block-title
, which other selectors in the theme use via :extend(.abs-block-title)
.) This is similar to a mixin but is a simpler construct.
Whew! That was as lengthy a bird’s-eye view as we could want. Now that we have a good idea of Magento’s Less structure, let’s take a look at the best way to leverage those components, by way of some practical use cases you might run into.
I Want to Significantly Re-factor Large Theme Areas
We’re starting with the most ambitious use case, because it also involves the most straightforward method of customization. Any Less partial can be directly overridden by a file in the appropriate path in your theme.
- Override
/app/code/MyCoolVendor/MyCoolApp/view/frontend/web/css/source/_module.less
in this location in your theme:MyCoolApp/web/css/source/_module.less
- Override
/lib/web/css/source/lib/_buttons.less
in this location in your theme:web/css/source/lib/_buttons.less
- Override the Magento/blank file
web/css/source/components/_breadcrumbs.less
in the same path in your own theme.
When you override a Less partial in this way, it entirely replaces the source file from further back in the inheritance chain or from an extension or lib source. This is the most direct way of modifying a parent theme or extension, but it is also the most unsuitable for most cases, because it involves copying forward the entirety of the original file. This method should only be used if you truly intend a wholesale overhaul that retains little of the original.
I Want to Swap the Color Palette or Change Some Aesthetic Details
Maybe you’re just interested in sprucing the native theme up with some changes in font and hue. Or perhaps you’ve created an awesome theme and want to use it on multiple stores with some color palette swapping, using themes that inherit from the first. Such modifications are precisely the use case for web/source/_theme.less
.
Less variables are used prevalently throughout Magento’s core themes (and should be in yours as well) for everything from font families and sizes to colors to spacing. By convention, _theme.less
is intended to be used solely for overriding the values of such variables. This technique is a fairly powerful method for achieving a unique aesthetic without touching a single line of CSS, and even if you intend to go further with your theme, you’ll no doubt make heavy use of it.
Especially regarding your color palette, note that it’s worthwhile to make use of self-contained Less variables within _theme.less
to keep things well organized. The following shows an example of defining color values only once and then distributing those values to other more meaningful variables:
@mytheme-color-gray: @color-gray22;
@mytheme-color-red: #a11e11;
@mytheme-color-red-light: lighen(@mytheme-color, 15%);
@primary__color: @mytheme-color-gray;
@link__color: @mytheme-color-red;
@link__hover__color: @mytheme-color-red-light;
@header__background-color: @mytheme-color-gray;
@h1__font-color: @mytheme-color-red;
This is the only context in which color names should be referenced in variable names. Use semantic, meaningful names for any new variables you create to be used elsewhere.
I Want to Make Major Customizations to Existing Styles
If the section title seems a little generic, that’s because we’re talking about the meat and potatoes of theming – the areas where you’ll be spending most of your time. This is the middle road between just changing some variables to alter the look and totally blowing away the native theme’s styles in favor of your own structure. This is still using the parent theme as a backbone but building your own customizations from there.
As noted above, you could accomplish this with direct overrides of files from the parent theme, but this is inadvisable. Doing so involves unnecessary duplication of large portions of code, and it makes it much more difficult to identify the styles that are unique to your theme at a glance. Instead, use _extend.less
for add-on styles that make your theme-specific modifications.
We’ve seen that _module.less
and _extend.less
are imported in the same way, and the difference between them is merely one of convention. Extensions and the native theme include the former but not the latter; you can think of _extend.less
as the empty space that’s left available for your theme-specific styles. As a caveat, though, once _extend.less
is implemented in a theme, any descendent themes that need to modify it will need to copy it forward like any other file.
Recall that _extend.less
is imported not just from a single location in your theme, but from multiple possible locations. While you might be tempted to use web/css/source/_extend.less
as a catch-all for your theme’s styles, you’re encouraged to split them into the appropriate extensions based on the areas they interact with. (e.g., build on what’s established in the Magento/blank file Magento_Customer/web/css/source/_module.less
in Magento_Customer/web/css/source/_extend.less
in your own theme.) This makes for a better organized and more maintainable theme structure.
While on the subject of general theming, a couple more general tips: First of all, make use of the mixins that the core UI library makes available whenever appropriate. Icons are a good example. Magento natively uses an icon font, generally utilizing :before and :after pseudo-elements to inject the right characters. While it would be possible to accomplish this manually in additional areas where you want to use icons, you’re better off using the .lib-icon-font
mixin as the core code does. This ensures better consistency and stability, and the native mixin implements the -webkit-font-smoothing
property for proper antialiasing, something that is easy to overlook.
Finally, avoid hard-coded values for things like colors and numerical values in your styles. Define variables in _theme.less
for such values, even if they’re new vars of your own invention.
I Want to Create Styles to Accompany My Extension
In the event that you’ve written an extension to add custom functionality that affects the Magento frontend, you’re going to need CSS to accompany it. For example, say you’ve created MyCoolVendor_Subcategories, which implements the ability for category pages to show a dynamic list of subcategories. By now, you probably know exactly where the associated styles go: view/frontend/web/css/source/_module.less
in the extension root directory.
Before we leave the topic, however, let’s talk about Less variables in this context. Your extension styles almost certainly should be using variables, and it’s likely they’ll be unique vars that don’t exist in the native theme. In our use cases thus far, new variables can be defined in _theme.less
with impunity, because those use cases were contained within the confines of a single theme, and Less’s lazy evaluation means variables can be defined as late as we please. In the case of an extension, however, we’ve arrived at a scenario where our Less should be complete without reference to a particular theme. Your new variables should be defined with default values in _module.less
itself, typically at the very top. This is a gimme if you’re developing a distributable extension, but it’s easy to miss when writing custom functionality for a specific site. If you do miss it, and if you compile static content without specifying a theme, expect to see Less compilation errors about non-existent variables when the core themes like Magento/blank are processed.
I Want to Override a Mixin
Let’s say you’d like to make some tweaks to Magento’s native styles, but the styles in question are embedded within the body of a mixin. For example, you might want a border radius on every element where the .lib-button
mixin is used. You could track down every selector where it’s used and implement your style on those selectors, but it would be better if you could inject it into the body of the mixin itself. (A good strategy would be to add a border radius parameter to the mixin definition, establishing a variable to represent a default value, which could then be set in _theme.less
.)
This could be a legitimate case for simply copying forward the file (web/css/source/lib/_buttons.less
) that defines that mixin. Note, though, that many such files group multiple related mixins together, and therefore there is some unnecessary duplication if we want to override only one. You might try implementing your mixin override in a path like web/css/source/custom/lib/_buttons.less
.
With this strategy, we’ve reached a scenario where we actually have a new Less partial that Magento will not import by default. This is where the manifest files we reviewed above come into play. Since web/css/_styles.less
and its subordinates are merely lists of imports, it’s not very intrusive at all to copy them forward and modify them. You could copy _styles.less
itself forward and add your new file to the list there; this has the advantage of less duplication of core files if you have multiple new imports to add. Or if you want to be very exact about placing your file in the exact same place in the import order as the original, you could copy forward that particular manifest file (in this case, web/css/source/lib/_lib.less
). Either is a fine approach.
As a final note, you might be tempted to think of overriding a mixin definition in this way as analogous to overriding a method in a programming language. It’s really not; mixin definitions are cumulative (something demonstrated clearly by the media breakpoint mixins). But Less also has enough magic at its fingertips to avoid outputting identical style declarations multiple times, so the result is still much the same. If you find you’re wanting to refactor the original mixin definition substantially, though, you’re probably better off creating a new mixin.
I Want to Define My Own Mixin
Speaking of creating your own mixin, that’s our last use case. Let’s say you create a flexbox-based implementation of a tabbed interface, which is a great case for encapsulation within a mixin. The best location for this is in lib
, like the native mixins: web/css/source/lib/_flextabs.less
. And you’ll want to make sure that, like the native mixins, yours supports default values for its parameters via named variables:
.lib-flextabs(
@_tab-background-color: @flextab__tab-background-color,
@_content-background-color: @flextab__content-background-color
// ... many other parameters, I'm sure
) {
// Styles go here ...
}
Now, wherever .lib-flextabs
is used, @_tab-background-color
can be passed in if desired. But the variable @flextab__tab-background-color
is also available to set universally in _theme.less
to apply to all uses of .lib-flextabs
when that parameter is not explicitly passed.
Presuming you’re defining your custom mixin within a theme, you could just put your initial declaration of all such default variables directly in _theme.less
and call it done. It’s probably better form, though, and will make your Less structure more intuitive for other devs, if you follow the core pattern and pair your mixin file with a variables file that defines default values. (e.g., web/css/source/lib/variables/_flextabs.less
)
From here, it’s just a matter of actually making sure your new Less partials get imported, which follows the same procedure described above: Copy forward web/css/_styles.less
and add them there, or else copy forward both web/css/source/lib/_lib.less
and web/css/source/lib/_variables.less
and add them in precisely the same spot that other mixins are imported.
In the event that you’re defining a new mixin as part of a custom extension, the procedure will be much the same. You’ll still define partials for your mixin definition and its variable default values, but you’ll put the import statements for them in your extension’s _module.less
.
Conclusion
I hope that in our list of use cases we’ve hit on a few practical scenarios you face in your day-to-day Magento development. If you’ve struggled with the right Less code to write and the right place to put it, you should have a better level of confidence now that we’ve peeked under the hood. Taking the time to examine how Magento understands and processes your theme code is well worth it to attain a theme that’s more understandable, more extensible, and more maintainable in the long run.