LESS and Styles
Extend with _extend.less, override variables in _theme.less, and compile with grunt or the static deploy command.
<theme_dir>/web/css/source/_extend.less | <theme_dir>/<Vendor>_<Module>/web/css/source/_extend.lessTheme 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
// 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:flushWhen 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.
// 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.
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 LiveReloadThe 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.
// 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