← Back to blog

Magento 2 LESS Customization: _theme.less, _extend.less and More

Rodrigo Biazzi

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:

  1. The UI library from lib/web/css/source/lib/, which provides mixins and default variables.
  2. The theme variables.
  3. Every module's _module.less and _widgets.less, through the special //@magento_import directive.
  4. The theme's _theme.less.
  5. 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.less in that folder adds styles for that module. It is picked up by the same //@magento_import as the theme level one.
  • _module.less in 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-face declarations in the compiled file.
  • Consider critical CSS. Magento can inline web/css/critical.css from your theme and load the rest asynchronously when dev/css/use_css_critical_path is 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