Maggie 3.0Meet the newly updated AI tutor

See what's new
Set up & configure Grunt for Magento 2 theming

Set up & configure Grunt for Magento 2 theming

Getting your development tooling right is important for efficient Magento 2 theming, and a crucial part of that process is setting up & configuring Grunt for LESS & CSS compilation.

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.

Grunt logo

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.

Less logo

(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:

  1. Put Magento in developer mode:
bin/magento deploy:mode:set developer
  1. 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-cli

Before 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.sample
  • Gruntfile.js.sample
  • grunt-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.json

After doing so, all npm dependencies can be installed with:

npm install

This 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.js

Working 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 in grunt exec:acme.
  • area is frontend for a storefront theme, or adminhtml for an admin theme.
  • name is the theme path in Vendor/theme format, matching app/design/frontend/Acme/default.
  • locale is the locale your store uses (Stores > Configuration > General > General > Locale Options). Add one entry per locale if you run more than one.
  • files lists the root stylesheets to compile, without the .less extension. A theme that inherits from Blank or Luma uses css/styles-m and css/styles-l.
  • dsl is the stylesheet language, which is less.

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:

CommandDescription
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 watchWatches 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 refreshRuns 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 watch

grunt 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 .less file. grunt watch only sees files that grunt exec already symlinked into pub/static. Run grunt exec:<theme> again, then grunt less:<theme>.
  • The theme isn't registered. If grunt exec:<theme> says the task isn't found, the key is missing from local-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:clean after 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.

Something here out of date?

Magento moves, and so does this site. If a step no longer works, tell me and I'll fix it.

Get the next one by email

Every other week I send a short newsletter on how PHP + Magento actually work: releases, security alerts, and one thing explained under the hood. Enroll in the Library and it's yours. No credit card needed.

The University includes every PHP + Magento course available, and every new M.academy course as it ships.