mrkeyoor.com_
Thu 06 Aug 15:38 UTC
npmWeb Frontendupdated 06 Aug 2026

less

Less is a CSS preprocessor: you write .less files with variables, nested rules, mixins, functions and arithmetic, and the compiler turns them into plain CSS that browsers understand. It has been around since 2009, it is written in JavaScript so it runs in Node and in the browser, and the package gives you both a library API (less.render) and a CLI binary called lessc. The language is deliberately close to CSS, so any valid CSS file is also a valid Less file, which is why migrating an existing stylesheet is usually a rename. Today most new projects reach for Sass instead, but Less is still the language under a large amount of shipped code: Bootstrap 3, Ant Design v4 themes, and countless internal design systems from the 2013 to 2019 era.

Verdict

Keep Less if your codebase is already Less, since it is maintained, still shipping releases, and migrating a large theme buys you very little. Do not choose it for a new project: Sass or plain PostCSS will be easier to hire for and easier to keep building in five years.

API stability4/5The 4.x line has held since 2020 and the render API has not changed shape, but 4.7.0 and 4.8.0 both added deprecation warnings for syntax that Less 5.x will remove, so stylesheets written years ago now emit warnings.
Docs3/5lesscss.org documents every function and option with examples, but the README is a monorepo stub that sends you elsewhere, the site's coverage of the Node API is thin, and important v4 behavior changes such as the math default are easy to miss until output breaks.
Maintenance4/5Pushed August 2026 with 4.8.1 released in July 2026 and a steady stream of small fixes, though 176 open issues and a release history dominated by one maintainer make the bus factor a fair concern.
Ecosystem4/5Around 11.6 million weekly downloads, first-class support in Vite, webpack via less-loader, and Ant Design and Bootstrap 3 era themes, but new tooling and component libraries increasingly assume Sass or plain CSS.

Use it if

  • You maintain a codebase that is already Less, such as a Bootstrap 3 theme or an Ant Design v4 app, and the job is to keep it building rather than to rewrite it
  • You need runtime theming: less.render or the less-loader modifyVars option lets you recompile with new variable values at runtime, which is how Ant Design style theme switchers were built
  • You want mixins, guards and arithmetic without adding a Dart or Ruby toolchain, since less is a plain npm dependency that runs anywhere Node runs
  • You compile CSS inside a Node process (a template renderer, a docs generator, an email builder) and want a Promise-returning render function rather than shelling out to a binary
Skip it if

Setup reality

npm install --save-dev less gives you the lessc binary and the Node API, and it needs Node 18 or newer. The first surprise is math: Less 4 changed the default to parens-division, so a stylesheet written for Less 3 that does @width / 2 outside parentheses now emits the slash verbatim instead of dividing, which shows up as broken layout rather than a build error. The second is that inline JavaScript in backticks is off by default since v4 and needs javascriptEnabled: true, which older themes rely on. The third is that seven of the nine dependencies are optionalDependencies (needle for remote @import over http, probe-image-size for the image-size functions, source-map, make-dir, mime, errno, graceful-fs), so an install run with --no-optional or a locked-down CI mirror produces a less that throws only when you hit those features. The package ships no TypeScript types either, so add @types/less if you call the API from TypeScript. In bundlers you do not use lessc: Vite compiles .less automatically once less is installed, and webpack needs less-loader in the chain before css-loader.

Patterns

Compile a file with lessc and emit a source mapcompile-file-with-cli

npx lessc src/styles.less dist/styles.css

# with a source map and compression via a plugin
npx lessc --source-map=dist/styles.css.map src/styles.less dist/styles.css
npm install --save-dev less-plugin-clean-css
npx lessc --clean-css src/styles.less dist/styles.min.css

lessc has no built-in minifier since v3; --clean-css only works after installing less-plugin-clean-css. Without --source-map the compiler emits no map at all, even when a bundler expects one.

Compile a string from Node with the Promise APIrender-from-node

const less = require('less')
const fs = require('node:fs/promises')

const input = await fs.readFile('src/styles.less', 'utf8')
const output = await less.render(input, {
  filename: 'src/styles.less',
  paths: ['src', 'node_modules'],
  math: 'parens-division',
})

console.log(output.css)
console.log(output.imports) // every file pulled in by @import, useful for watch mode

Pass filename even for string input, otherwise relative @import paths resolve against the process cwd and url() rewriting has no base to work from. render returns a Promise when you omit the callback.

Get division working the way you expect in Less 4math-mode

@gutter: 24px;

.card {
  // parens-division (the v4 default): this stays as literal "24px / 2"
  padding: @gutter / 2;
  // this divides
  margin: (@gutter / 2);
  // plus, minus and times still evaluate everywhere
  width: @gutter * 4;
}

Set math to 'always' to restore Less 3 behavior, or 'parens' to require parentheses for every operation. Silent wrong output rather than an error is the reason most Less 3 to Less 4 upgrades look fine until someone opens the page.

Understand lazy evaluation and last-wins variablesvariables-and-scope

@brand: blue;

.header {
  color: @brand;   // resolves to green, not blue
  @brand: green;   // later in the block still wins
}

// redefining at the same level overwrites for the whole scope
@brand: red;
.footer { color: @brand; } // red

Less variables are lazily evaluated and the last definition in a scope wins, no matter where the reference sits. There is no module system, so two imported files declaring @brand collide silently. Prefix variables per component if the codebase is large.

Write a parametric mixin with a guardmixins-and-guards

.button-variant(@bg; @radius: 4px) when (iscolor(@bg)) {
  background: @bg;
  border-radius: @radius;
  color: if((lightness(@bg) > 50%), #111, #fff);
}

.btn-primary { .button-variant(#2563eb); }
.btn-danger  { .button-variant(#dc2626; 8px); }

Use semicolons to separate mixin arguments; commas are treated as a single comma-separated value, which is the classic source of 'wrong number of arguments' errors. A mixin defined as .name() with empty parens is not emitted as a CSS class.

Import without duplicating outputimport-options

@import (reference) "../theme/variables.less"; // pull in mixins and vars, emit nothing
@import (optional) "./local-overrides.less";    // no error if the file is missing
@import (css) "https://fonts.example.com/x.css"; // leave as a plain CSS @import
@import (inline) "./legacy.css";                 // paste through unparsed

Without (reference), importing a shared file into ten entry points emits its rules ten times. (inline) is the escape hatch for CSS that Less cannot parse, and remote imports over http need the optional needle dependency present.

Escape values and interpolate into selectors and propertiesescaping-and-interpolation

@min768: ~"(min-width: 768px)";
@prefix: app;
@side: left;

@media @min768 { .x { display: flex; } }

.@{prefix}-card {
  margin-@{side}: 12px;
  width: ~"calc(100% - @{gutter})";
}

Curly braces are required inside strings and identifiers: @{prefix} interpolates, a bare @prefix does not. Wrapping calc in ~"" stops Less from trying to evaluate the arithmetic itself; in Less 4 plain calc() is already left alone, so only reach for the tilde when the compiler mangles something.

Share rules with :extend instead of duplicating declarationsextend-vs-mixin

.btn { padding: 8px 16px; border: 0; }
.btn:hover { opacity: .9; }

.btn-link {
  &:extend(.btn all); // 'all' also copies .btn:hover and other .btn-derived selectors
  background: none;
}

extend adds your selector to the original rule instead of copying declarations, so the CSS stays smaller than with a mixin. The tradeoff is source order: the extended rule keeps the position of the original, which can flip specificity outcomes you expected from a mixin.

Loop over a map to generate utility classesmaps-and-each

@spacings: {
  sm: 4px;
  md: 8px;
  lg: 16px;
};

each(@spacings, {
  .p-@{key} { padding: @value; }
});

// direct lookup
.gap { gap: @spacings[md]; }

each() gives you @key, @value and @index. Map lookup with square brackets needs Less 3.5 or newer, and the key is not quoted. Generating hundreds of classes this way slows compilation noticeably on large files.

Recompile with overridden variables for theme switchingruntime-theming

const less = require('less')

async function buildTheme(primary) {
  const { css } = await less.render('@import "theme/index.less";', {
    filename: 'virtual.less',
    paths: ['src'],
    modifyVars: { '@primary-color': primary },
    javascriptEnabled: true, // older themes use backtick JS in their mixins
  })
  return css
}

modifyVars injects declarations after everything else, so they override the theme defaults. This is the mechanism behind Ant Design v4 theme switchers; it recompiles the whole stylesheet each time, so cache the result rather than calling it per request.

Wire Less into Vite or webpackbundler-config

// vite.config.js: just install less, then configure options if needed
export default {
  css: {
    preprocessorOptions: {
      less: { math: 'parens-division', javascriptEnabled: true },
    },
  },
}

// webpack.config.js
module.exports = {
  module: {
    rules: [{
      test: /\.less$/i,
      use: ['style-loader', 'css-loader', {
        loader: 'less-loader',
        options: { lessOptions: { math: 'parens-division' } },
      }],
    }],
  },
}

Vite needs no plugin, only the less package on disk. In webpack the loaders run right to left, so less-loader must be last in the array; putting it first produces 'Unexpected character @' errors from css-loader.

Add a custom function with a Less pluginplugin-and-functions

// plugin.js
module.exports = {
  install(less, pluginManager, functions) {
    functions.add('double', (value) =>
      new less.tree.Dimension(value.value * 2, value.unit))
  },
}

// styles.less
@plugin "./plugin";
.x { width: double(20px); } // width: 40px

This is the supported replacement for inline backtick JavaScript, which is disabled by default in Less 4. Plugin functions receive Less node objects, not plain numbers, so you have to read .value and construct a tree node to return.

Alternatives

PackageRegistryPick it when
sassnpmYou are picking a preprocessor today and want the one with the module system, the ecosystem and the active maintenance.
postcssnpmYou want to stay in plain CSS syntax and add only the transforms you actually need, such as nesting and autoprefixing.
stylusnpmYou want terse optional-brace syntax and a JS-first plugin API, and you accept a much smaller community than either Less or Sass.