Magento 2 LESS Customization: _theme.less, _extend.less and More
Luma and Blank are styled with LESS on top of the Magento UI library. The system is flexible, but it has its own file naming conventions and import rules, and if you do not follow them you end up with a theme that copies half of Luma and breaks on every upgrade.
This article explains how LESS is organised in a Magento 2.4.8 theme, which file to touch for which kind of change, and how to compile it quickly while you work.
How the stylesheets are built
A Luma based storefront loads two main CSS files: styles-m.css for mobile and common styles, and styles-l.css for desktop styles. Each comes from a LESS root file in the Blank theme, web/css/styles-m.less and web/css/styles-l.less. These files import, in order:
- The UI library from
lib/web/css/source/lib/, which provides mixins and default variables. - The theme variables.
- Every module's
_module.lessand_widgets.less, through the special//@magento_importdirective. - The theme's
_theme.less. - Every
_extend.less, again through//@magento_import.
//@magento_import looks like a comment but is processed by Magento before the LESS compiler runs. It collects the file with that name from every module and from the theme fallback chain. This is why the file names below matter. A file with a different name is simply never imported.
The files you should edit
_theme.less: change variables
web/css/source/_theme.less is where you override UI library and parent theme variables. A child theme of Luma inherits Luma's _theme.less, but if you create your own file with the same name, it replaces the parent file completely instead of adding to it. So start by copying the content of the parent file from vendor/magento/theme-frontend-luma/web/css/source/_theme.less into your theme, then change or append variables:
// copied Luma variables above this line
@primary__color: #1a3c5e;
@link__color: #1a3c5e;
@link__hover__color: darken(#1a3c5e, 10%);
@font-family__base: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
Forgetting the copy step is the most common LESS mistake in child themes. The parent values disappear and parts of the store suddenly fall back to Blank colours. The cost of the copy is that you need to compare the file with the parent on each upgrade, so keep your own changes at the bottom where they are easy to find.
Before overriding a variable, search for it in lib/web/css/source/lib/variables/ and in the parent theme. Changing a variable is the cheapest customisation there is, because every component that uses it updates at once.
_extend.less: add styles
web/css/source/_extend.less is for new rules. It is imported after everything else, so it wins on equal specificity. Keep it as an index and split the real work into partials:
@import 'components/_header.less';
@import 'components/_product-card.less';
@import 'components/_footer.less';
Use _extend.less by default. It adds to the parent theme instead of replacing it, so parent fixes still reach you after an upgrade.
Module level files
Each module folder in the theme can have its own styles, for example Magento_Catalog/web/css/source/:
_extend.lessin that folder adds styles for that module. It is picked up by the same//@magento_importas the theme level one._module.lessin that folder replaces the parent theme's styles for that module. Use it only when you are restyling the module from scratch.
I see a lot of themes with a copied _module.less where only a few lines changed. That is the LESS version of copying a template to change one line, and it has the same upgrade cost.
Media queries the Magento way
Styles are split between the two CSS files with guarded mixins. Rules for all screen sizes go in a block guarded by @media-common, and screen specific rules use the .media-width() mixin:
& when (@media-common = true) {
.product-card {
border: 1px solid @border-color__base;
padding: @indent__s;
}
}
.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) {
.product-card {
padding: @indent__base;
}
}
.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) {
.product-card {
padding: @indent__xs;
}
}
Magento compiles the min breakpoints from @screen__m up into styles-l.css, and the rest into styles-m.css. If you write plain @media blocks instead, the rules end up duplicated in both files or in the wrong one. Writing rules outside a guard also duplicates them, which is a common reason for CSS files that grow with no visible change.
The breakpoint variables are @screen__xxs, @screen__xs, @screen__s, @screen__m, @screen__l and @screen__xl.
Compiling LESS during development
There are two compilation modes, set under Stores, Configuration, Advanced, Developer, Frontend Development Workflow (config path dev/front_end_development_workflow/type):
- Server side compilation is the default and the only option for production. Magento compiles LESS with a PHP library when the CSS file is requested in developer mode, or during static content deploy.
- Client side compilation compiles in the browser with less.js. It is slow and I do not recommend it.
For day to day work, Grunt is faster and gives you live reload. Magento ships the files you need:
cp package.json.sample package.json
cp Gruntfile.js.sample Gruntfile.js
cp dev/tools/grunt/configs/themes.js dev/tools/grunt/configs/local-themes.js
npm install
Add your theme to local-themes.js:
module.exports = {
vendortheme: {
area: 'frontend',
name: 'Vendor/theme',
locale: 'en_US',
files: [
'css/styles-m',
'css/styles-l'
],
dsl: 'less'
}
};
Then run the three commands below. The first publishes source files as symlinks, the second compiles, the third watches for changes:
npx grunt exec:vendortheme
npx grunt less:vendortheme
npx grunt watch
Run exec again whenever you add a new LESS file, because the symlinks only cover files that existed when it ran.
When styles do not update
Stale CSS is almost always a cache issue. Clear the compiled files for the theme and the preprocessed sources:
rm -rf pub/static/frontend/Vendor/theme var/view_preprocessed/pub/static/frontend/Vendor/theme
bin/magento cache:clean
In production mode, nothing changes until you run bin/magento setup:static-content:deploy for the theme and locales. The static content deploy strategies article covers how to keep that step fast.
Keeping CSS lean
Luma CSS is large, and every theme adds to it. A few rules help:
- Change variables before writing new rules.
- Avoid deep nesting. LESS makes it easy to write selectors five levels deep, which hurts both specificity and file size.
- Do not import fonts or icon sets you do not use. Check
@font-facedeclarations in the compiled file. - Consider critical CSS. Magento can inline
web/css/critical.cssfrom your theme and load the rest asynchronously whendev/css/use_css_critical_pathis enabled. You have to write or generate that file yourself.
If you are planning a bigger visual change, it is worth comparing the cost with a move away from LESS altogether. The Hyvä Tailwind customization article shows what styling looks like on that side. For setting up the theme folder itself, start with creating a child theme.
FAQ
Should I put my styles in _theme.less or _extend.less?
Variables go in _theme.less. New rules go in _extend.less. A _theme.less in your theme replaces the parent file, so copy the parent version into it first.
Why is my LESS file not compiled?
Magento only imports files with the expected names, such as _extend.less, _module.less and _theme.less, or files you import from them. A new partial must be imported from one of those, and with Grunt you need to run the exec task again.
Can I use Sass instead of LESS in a Luma theme?
Not with the built in pipeline. Some community themes ship a Sass port of the UI library with their own build tools, but core Luma and Blank are LESS only.
Why are my rules duplicated in styles-m.css and styles-l.css?
They were written outside a media-common guard or inside plain media queries. Wrap common rules in the media-common guard and use the .media-width() mixin for breakpoints.
I am available for freelance Magento and Adobe Commerce work, including Luma theme cleanup and frontend performance. If you need help with this, get in touch.
Related articles
How to Create a Magento 2 Child Theme Step by Step
Create a Magento 2 child theme of Luma or Blank: theme.xml, registration.php, composer.json, view.xml, logo, template overrides, applying and deploying it.
Magento 2 Layout XML: A Practical Guide for Developers
How Magento 2 layout XML works: handles, containers, blocks, move and remove, arguments, view models and page layouts, with examples that hold up in 2.4.8.
Writing Alpine.js Components in Hyvä: Patterns That Scale
How to write Alpine.js components on Hyvä: passing PHP data, view models, customer data events, form keys, component events, CSP and keeping templates clean.
Hyvä Compatibility Modules: Make Luma Extensions Work
How Hyvä compatibility modules work: the CompatModuleRegistry, rewriting extension templates with Alpine.js, hyva_ layout handles and the Luma fallback.