To use Grunt with Magento 2, copy package.json.sample, Gruntfile.js.sample and grunt-config.json.sample without the .sample suffix, run npm install, register your theme in dev/tools/grunt/configs/local-themes.js, then run grunt exec:<theme>, grunt less:<theme> and grunt watch. On docker-magento, bin/setup-grunt does all of it.
The rest of this article walks through each step, and what every Grunt command actually does under the hood.
What is Grunt?
Grunt is the task runner Magento ships with for compiling Less to CSS on the server while you develop a theme. It can also handle minification, linting and running tests, similar to a lot of other tools out there such as Gulp, Webpack & Vite. This is an important tool to use when developing custom Magento themes.
Normally when you write CSS, a task runner is not needed. But Magento's Luma and Blank themes use Less, which extends CSS to add support for variables, mixins, nesting, math operations, functions & more.
Less.js is a JavaScript compiler that converts this Less code to standard CSS. You’ll need a task runner to run it on every change, and Grunt supports Less compilation and is already built into Magento.
(Building on Hyvä instead? Hyvä themes use Tailwind CSS with their own npm build, so Grunt isn't part of that workflow at all. The Hyvä theme course covers that setup.)
Using docker-magento?
If you are using docker-magento, all of these steps are already taken care of. Run bin/setup-grunt, then use bin/grunt.
That script copies the three sample files, generates local-themes.js with your store's default theme already registered, runs npm install, then runs grunt clean, grunt exec:<theme> and grunt less:<theme> for you.
You can skip ahead to the Run Grunt commands section if you wish, just remember to prefix grunt with bin/ to use Grunt within the Docker container. But if you’d like to know how the Grunt setup works, just keep following along.
Before you start: developer mode and server-side Less
Grunt compiles Less on the server, so Magento has to be set up to match:
- Put Magento in developer mode:
bin/magento deploy:mode:set developer- In the admin, go to Stores > Configuration > Advanced > Developer > Frontend Development Workflow and make sure Workflow type is set to Server side less compilation. This is the default, and the Developer section only appears in developer mode.
Install Grunt
Assuming you already have Node.js installed on your machine, install the grunt command line globally with:
npm install -g grunt-cliBefore running grunt, you’ll also need to install some prerequisite packages. Magento comes with a few files in the root of your project:
package.json.sampleGruntfile.js.samplegrunt-config.json.sample
Copy all of these files to the same locations, but without the .sample suffix:
cp package.json.sample package.json
cp Gruntfile.js.sample Gruntfile.js
cp grunt-config.json.sample grunt-config.jsonAfter doing so, all npm dependencies can be installed with:
npm installThis looks at the package.json file that you just created, and installs all dependencies in that file, including Grunt itself, its plugins and the Less compiler.
Configure Grunt for Magento
Grunt works from a Gruntfile, which is basically a configuration file that contains a function with a list of instructions that Grunt executes. If you take a look at this file in the root of your Magento folder, you’ll see that it loads every task config from dev/tools/grunt/configs, and defines a few combined tasks like refresh.
Now open up the grunt-config.json file. This file tells Grunt where to find the list of themes it should work with:
{
"themes": "dev/tools/grunt/configs/local-themes"
}This value is just fine, but that local-themes.js file doesn't exist yet, and Grunt fails with a "Cannot find module" error until it does. Create it by copying the core themes.js file in the same directory:
cp dev/tools/grunt/configs/themes.js dev/tools/grunt/configs/local-themes.jsWorking from your own copy safeguards your changes, so they don’t get overwritten by core updates. (Leave out grunt-config.json entirely, and Grunt falls back to the core themes.js, which only knows about Blank, Luma and the admin theme.)
Register your custom theme
Out of the box, local-themes.js only lists blank, luma and backend. Grunt doesn't know your custom theme exists until you add it. Open dev/tools/grunt/configs/local-themes.js and add an entry for it:
module.exports = {
blank: {
area: "frontend",
name: "Magento/blank",
locale: "en_US",
files: [
"css/styles-m",
"css/styles-l",
"css/email",
"css/email-inline"
],
dsl: "less"
},
luma: {
area: "frontend",
name: "Magento/luma",
locale: "en_US",
files: [
"css/styles-m",
"css/styles-l"
],
dsl: "less"
},
backend: {
area: "adminhtml",
name: "Magento/backend",
locale: "en_US",
files: [
"css/styles-old",
"css/styles"
],
dsl: "less"
},
acme: {
area: "frontend",
name: "Acme/default",
locale: "en_US",
files: [
"css/styles-m",
"css/styles-l"
],
dsl: "less"
}
};Each property means:
- The key (
acme) is the name you pass to Grunt, as ingrunt exec:acme. areaisfrontendfor a storefront theme, oradminhtmlfor an admin theme.nameis the theme path inVendor/themeformat, matchingapp/design/frontend/Acme/default.localeis the locale your store uses (Stores > Configuration > General > General > Locale Options). Add one entry per locale if you run more than one.fileslists the root stylesheets to compile, without the.lessextension. A theme that inherits from Blank or Luma usescss/styles-mandcss/styles-l.dslis the stylesheet language, which isless.
Run Grunt commands
At this point, Grunt can be executed. If you are running docker-magento, be sure to prefix every command with bin/.
Running plain grunt executes the default task from the Gruntfile, which is empty. Every useful command lives in dev/tools/grunt/configs, where you'll see files such as clean.js, exec.js, less.js and watch.js. Each one is loaded automatically and becomes the Grunt task of the same name, with one target per theme registered in local-themes.js.
Here is a list of common Grunt commands used in Magento frontend development, and when to use each:
| Command | Description |
|---|---|
grunt clean:<theme> | Deletes that theme's generated files from pub/static, its var/view_preprocessed files, and var/cache. Plain grunt clean runs every clean target, which also wipes var/page_cache, all of pub/static and generated/. |
grunt exec:<theme> | Runs clean:<theme>, then bin/magento dev:source-theme:deploy to republish symlinks to the theme's Less source files in pub/static. Run it whenever you add, rename or remove a Less file. Plain grunt exec runs it for every theme. |
grunt less:<theme> | Compiles the theme's Less into CSS (styles-m.css, styles-l.css) inside pub/static, using the symlinks created by grunt exec. |
grunt watch | Watches the theme's Less files and runs less:<theme> when one changes. It also serves LiveReload when the CSS changes, so the browser reloads only if you have a LiveReload browser extension installed. |
grunt refresh | Runs clean, exec:all, then less for every registered theme. It's the "start from scratch" command when something looks stale. |
A good first run for a custom theme looks like this:
grunt exec:acme
grunt less:acme
grunt watchgrunt exec:acme takes the longest, because it shells out to bin/magento to clean and republish all of the theme's source files. You can see the exact command it runs in dev/tools/grunt/configs/combo.js:
command = command + 'php bin/magento dev:source-theme:deploy ' +
theme[themeName].files.join(' ') +
' --type=less' +
' --locale=' + theme[themeName].locale +
' --area=' + theme[themeName].area +
' --theme=' + theme[themeName].name;After that, grunt watch stands by. Rather than making a stylesheet update and re-running grunt less every time, you save a .less file, Grunt recompiles that theme's CSS, and LiveReload refreshes the page. This makes styling way quicker and takes away a lot of the friction of compiling styles.
Why aren't my Less changes showing up?
When a change doesn't appear, it's almost always one of these:
- You added a new
.lessfile.grunt watchonly sees files thatgrunt execalready symlinked intopub/static. Rungrunt exec:<theme>again, thengrunt less:<theme>. - The theme isn't registered. If
grunt exec:<theme>says the task isn't found, the key is missing fromlocal-themes.js. - The workflow type is set to client-side. Check Stores > Configuration > Advanced > Developer > Frontend Development Workflow again.
- Magento's cache still holds the old page. Grunt compiles CSS, but it doesn't touch the full page or layout cache. Run
bin/magento cache:cleanafter layout XML or template changes. (See cache clean vs. cache flush for which one to use.) - The browser cached the CSS. Hard-refresh, or keep DevTools open with "Disable cache" checked while you work.
Adobe's Compile Less with Grunt page covers the same workflow if you want the official reference alongside this one.
If you're just getting into custom themes, the Magento 2 theming course picks up from here. And if your team has a Grunt setup that works better than the stock one, I'd like to hear how you've changed it.



