LESS and Styles

Extend with _extend.less, override variables in _theme.less, and compile with grunt or the static deploy command.

Syntax<theme_dir>/web/css/source/_extend.less | <theme_dir>/<Vendor>_<Module>/web/css/source/_extend.less

Theme styles are written in LESS and compiled to CSS. A theme has three entry points in web/css: styles-m.less for the mobile-first base, styles-l.less for desktop, and print.less. styles-m.less and styles-l.less pull in _styles.less, which imports the shared UI library from lib/web/css; print.less skips _styles.less and imports source/lib/_lib.less directly. You rarely touch any of them.

Extend or override

There are two patterns. Extend: create <theme_dir>/web/css/source/_extend.less and write only your differences; its rules load after the parent's, so they win. For one module's styles use <theme_dir>/<Vendor>_<Module>/web/css/source/_extend.less. Override: create a file at the same path as the parent's and fallback picks yours instead, the usual cases being _theme.less for UI library variables and <Vendor>_<Module>/web/css/source/_module.less. There is no magic _override.less filename; overriding is path matching, and it means you now own that file.

Shorthand and longhand

//@magento_import 'source/_extend.less'; is Magento's own directive. It collects every file matching that path from the theme, its ancestors and all modules, and expands into one plain @import per file found. The longhand @import 'source/_extend.less'; is the same mechanism for a single file: it resolves one path through fallback. The directive must stay commented with two slashes and may appear only in a theme's root source files.

Compiling

Server-side compilation is the default. grunt less gives a fast loop in developer mode; otherwise bin/magento setup:static-content:deploy compiles.

Example

Example · less
// app/design/frontend/Acme/storefront/web/css/source/_extend.less

//  Loaded after the parent theme's styles, so these rules win without
//  copying anything from Luma. Variables come from the UI library.

.page-header {
    border-bottom: 1px solid @border-color__base;
}

.product-info-main {
    .price-box .price {
        font-size: 2.4rem;
        font-weight: @font-weight__bold;
    }
}

//  Breakpoints go through the responsive collector, not a raw @media block,
//  so the rules land in styles-l.css only.
.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) {
    .page-main {
        padding-top: @indent__m;
    }
}

//  Developer mode: delete pub/static/frontend and var/view_preprocessed,
//  or run grunt less:storefront, then reload. Production mode:
//  bin/magento setup:static-content:deploy -f en_US && bin/magento cache:flush

When to use it

  • Change the storefront's base font and heading sizes without copying Luma's stylesheets.
  • Restyle one module's blocks, such as the review summary, in isolation.
  • Set up a grunt watch loop so a saved .less file shows up after a reload.
  • Work out why a rule you wrote is being beaten by a parent theme rule.

More examples

Overriding variables in _theme.less

Variables are the cheapest customisation: changing a handful recolours the whole theme because the UI library derives the rest from them. The file is an override, so your _theme.less replaces the parent's: copy across every variable the parent set, including the ones you are not changing, or you will lose them. That copy is also the file you must re-check after a minor upgrade.

Example · less
// app/design/frontend/Acme/storefront/web/css/source/_theme.less

//  Colors
@color-acme-blue: #0b3d91;
@link__color: @color-acme-blue;
@border-color__base: @color-gray80;

//  Fonts
@font-family-name__base: 'Acme Sans';
@font-size__base: 1.5rem;

//  Buttons
@button__background: @color-acme-blue;
@button__border: 1px solid @color-acme-blue;
@button-primary__background: @color-acme-blue;
@button-primary__hover__background: darken(@color-acme-blue, 8%);

The grunt loop

Grunt is a developer-mode convenience, not a deployment tool: it compiles a theme's LESS straight into pub/static so a saved file is visible on reload. The sample configuration files ship uncopied so that yours is not overwritten by an upgrade, and your theme has to be listed in dev/tools/grunt/configs/themes.js with its area, name, locale and the files to build. For anything that is not your own machine, use setup:static-content:deploy.

Example · bash
cp Gruntfile.js.sample Gruntfile.js
cp package.json.sample package.json
cp grunt-config.json.sample grunt-config.json
npm install

# Register the theme in dev/tools/grunt/configs/themes.js:
#   storefront: {
#       area: 'frontend',
#       name: 'Acme/storefront',
#       locale: 'en_US',
#       files: ['css/styles-m', 'css/styles-l'],
#       dsl: 'less'
#   }

grunt clean:storefront     # remove the generated static files
grunt exec:storefront      # republish the root source files
grunt less:storefront      # compile after editing any .less file
grunt watch                # recompile on save, with LiveReload

The directive and the plain import side by side

This is Blank's styles-m.less, lightly trimmed. //@magento_import is the shorthand: one line collects _module.less from the theme, its ancestors and every module that ships one, and expands to a plain @import for each. @import is the longhand: one line, one file, resolved once through fallback. If you need a file from exactly one known place, write @import; if you want whatever every module contributed, write the directive.

Example · less
// app/design/frontend/Magento/blank/web/css/styles-m.less

//  Longhand: each line resolves one path through fallback
@import 'source/_reset.less';
@import '_styles.less';

//  Shorthand: each line collects every matching file and expands
//  into one @import per file found
//@magento_import 'source/_module.less';   // Theme modules
//@magento_import 'source/_widgets.less';  // Theme widgets

@import 'source/_theme.less';              // variables override

//@magento_import 'source/_extend.less';   // minor customisation

@import 'source/lib/_responsive.less';     // media queries collector

@media-target: 'mobile';

Discussion

  • Be the first to comment on this lesson.