sass/node-sass
A widely used Node.js binding for libsass that compiles Sass/SCSS stylesheets to CSS within JavaScript build pipelines. It offers fast native compilation and broad tooling integration, but is deprecated and maintained only for legacy projects; new work should use Dart Sass.
What you're actually getting. node-sass is a native C++ binding to LibSass. In a Laravel codebase you almost never choose it — it arrives as a transitive dependency of laravel-mix@5 / sass-loader@8–10 (Laravel 6/7/8-era projects). It is archived and deprecated: LibSass was retired in favour of Dart Sass in 2020, Node 21+ has no prebuilt binary, and no fixes are coming. Treat anything you learn here as maintenance and migration work, not greenfield tooling.
First 10 minutes in an unfamiliar Laravel repo:
# 1. Is it even here, and who pulled it in?
npm ls node-sass
grep -n "node-sass" package.json package-lock.json | head
# 2. Which Node ABI do you need a binary for?
node -v && node -p "process.versions.modules" # e.g. 18.x -> 108
# 3. Where Mix wires up Sass
cat webpack.mix.js
Where to look first, in order:
| File | What you're checking |
|---|---|
package.json |
Pinned node-sass range, laravel-mix version, sass-loader version, fibers |
webpack.mix.js |
mix.sass(...) entry points and any sassOptions / includePaths |
resources/sass/app.scss |
@import graph — this is where LibSass limits bite |
.npmrc / CI env |
sass_binary_site, proxy, offline mirrors |
node_modules/node-sass/vendor/ |
Presence of <platform>-<arch>-<abi>/binding.node |
Canonical first use case — you have a fresh clone and npm install/npm run dev explodes.
npm ci # fails: "Node Sass could not find a binding
# for your current environment: Linux 64-bit with Node.js 18.x"
npm rebuild node-sass # try first; downloads the matching prebuilt binary
# still failing? confirm the matrix before touching code:
node -p "require('node-sass/package.json').version"
If npm rebuild can't produce a binary (Node 21/22, Alpine/musl, ARM edge cases), you have exactly two moves: pin Node to a supported version in .nvmrc / Dockerfile, or migrate to sass (see Implementation Patterns). There is no third option.
Baseline dev loop once it works — nothing exotic:
npm run dev # one-shot compile
npm run watch # incremental, used while editing resources/sass/**
npm run production # minified, sourcemaps off
Your webpack.mix.js is unchanged whether the underlying compiler is node-sass or dart-sass. That's the point: the migration surface is package.json, not application code.
// webpack.mix.js — Mix 5 style, node-sass under the hood
const mix = require('laravel-mix');
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.options({ processCssUrls: false }); // don't rewrite url() via resolve-url-loader
Pass compiler options through to sass-loader. The third-argument shape differs by Mix version — check node_modules/laravel-mix/package.json before copying snippets from the internet:
// Mix 6+ / sass-loader 10+ shape
mix.sass('resources/sass/app.scss', 'public/css', {
sassOptions: {
includePaths: ['node_modules'],
outputStyle: 'compressed',
precision: 5, // node-sass-specific; ignored by dart-sass
sourceMap: mix.inProduction() ? false : true,
},
});
includePaths: ['node_modules'] is the useful one: it lets @import 'bootstrap/scss/bootstrap'; resolve without the ~ prefix, and it's the single option that survives the dart-sass migration unchanged.
The idiomatic Laravel layout works fine with node-sass. Keep the entry file thin and let LibSass's importer do the walking:
// resources/sass/app.scss
@import 'variables';
@import 'mixins/buttons';
@import 'components/navbar';
@import 'pages/dashboard';
mix.sass('resources/sass/app.scss', 'public/css')
.sass('resources/sass/admin.scss', 'public/css');
Two entry points beat one giant file: npm run watch recompiles only what changed, and node-sass (native, single-threaded) is noticeably slower than dart-sass, so smaller graphs matter.
Legitimate uses: generating a static CSS artifact at deploy time, or a Laravel artisan-adjacent Node script for a theme builder. Keep it out of the webpack pipeline so it can't break the frontend build.
// scripts/build-theme.js
const sass = require('node-sass');
const path = require('path');
const result = sass.renderSync({
file: 'resources/sass/theme.scss',
includePaths: [path.resolve('node_modules')],
outputStyle: 'compressed',
// Custom importer: resolve vendor-provided partials
importer: (url) =>
url.startsWith('@brand/')
? { file: path.resolve('vendor/brand', '_' + url.slice(7) + '.scss') }
: null,
});
require('fs').writeFileSync('public/css/theme.css', result.css);
For the async API, render({...}, (err, result) => {}); the importer signature becomes (url, prev, done). importer is the main extension point — it's how you bridge Composer-installed packages (vendor/) into Sass without copying files.
const sass = require('node-sass');
sass.renderSync({
file: 'resources/sass/app.scss',
functions: {
'asset-url($path)': (p) => sass.types.String(`url("/storage/${p.getValue()}")`),
'brand-color($shade)': (s) => sass.types.Color(0, 123, 255),
},
});
Useful for injecting asset()/mix()-style hashed paths at build time without hardcoding. Note that these functions are not portable to dart-sass — dart-sass takes Value/LegacyValue objects with a different call signature, so any function you write here is migration debt.
Mix 6+ and Vite (Laravel 9+) already use dart-sass. The swap is mechanically small:
npm uninstall node-sass fibers
npm install -D sass@^1.70
- "laravel-mix": "^5.0.0",
+ "laravel-mix": "^6.0.0",
- "sass-loader": "^8.0.0",
+ "sass-loader": "^12.0.0",
Then audit the Sass source for LibSass-isms (next section). Order of operations that works: migrate the toolchain first and let the deprecation warnings list the offending files, then fix Sass file-by-file, then delete fibers (it throws on Node 16+ anyway).
The binary binding is the whole story. node-sass downloads a prebuilt .node per (platform, arch, Node ABI) into node_modules/node-sass/vendor/. Any mismatch — a Node upgrade, a lockfile from another machine, a Docker base image change — produces:
Error: Node Sass does not yet support your current environment: Linux 64-bit with Node.js 21.x
Triage ladder, in order:
node -p "process.versions.modules" → confirm the ABI.npm rebuild node-sass → download the matching binary.node-sass to the highest version that supports your Node (v9.0.0 covers Node 20; nothing covers 21+)..nvmrc, FROM node:18-bullseye) — the pragmatic answer for a legacy app.Don't use Alpine images. node:18-alpine is musl, and prebuilt binaries are glibc-only, so npm falls back to compiling from source — which needs Python and a full C++ toolchain, and usually fails on binding.gyp. Use -slim/-bullseye, or install python3 make g++ in the image and accept 5-minute builds.
Lockfile churn in CI. Never run npm install in CI. npm ci fails loudly on package-lock.json drift; cache ~/.npm and node_modules keyed on hashFiles('**/package-lock.json') plus the Node version, or the stale vendored binary gets restored and you debug a phantom error for twenty minutes.
Offline / corporate network installs. The binary comes from GitHub releases, not the npm registry, so registry mirrors don't help:
# .npmrc
sass_binary_site=https://npmmirror.com/mirrors/node-sass/
Or pre-seed the binary: SASS_BINARY_PATH=/opt/binaries/linux-x64-108_binding.node npm ci. In Docker, set SASS_BINARY_SITE as a build arg rather than baking a mirror into .npmrc (it will rot).
LibSass ≠ Sass. The feature gaps that actually break Laravel projects:
@use / @forward. LibSass 3.6 never implemented the module system. Any modern Sass library (@use-based) will not compile. Only @import works, with all its global-namespace problems.math.div(), map.get(), color.adjust(), meta.*, selector.* are all missing. You're on the legacy global functions (map-get, lighten, unquote).min() / max() are Sass functions, not CSS. min(100%, 500px) throws "Incompatible units" under LibSass; dart-sass passes it through as CSS. If you have responsive min()/clamp() in your Sass, this is the migration bug that bites hardest and looks like a syntax error./ is silently fine in node-sass; in dart-sass it's deprecated and needs math.div(). So the deprecation-warning noise after migration is expected, not a regression.@media quirks around & differ subtly at the edges.Debug the toolchain, not the CSS. When a build error is opaque:
DEBUG=* npm run dev 2>&1 | grep -i sass # see loader/compiler handoff
npm ls sass-loader node-sass # version skew is the usual culprit
rm -rf node_modules package-lock.json && npm install # last resort, but it works
node -e "require('node-sass'); console.log('binding ok')"
That last line isolates "broken binary" from "broken Sass" in one command — run it before reading a single line of your .scss.
Sourcemaps and url() rewriting. node-sass emits sourcemaps only when sourceMap: true and sourceMapContents is passed; Mix handles this, but if you call render yourself you'll get a map pointing at a temp path. Separately, processCssUrls: true (the Mix default) sends url() through resolve-url-loader, which historically produced bizarre rewritten paths with node-sass. Most Laravel projects turn it off and let mix()/asset() handle URLs — that's why .options({ processCssUrls: false }) is in every legacy webpack.mix.js.
fibers is a red herring now. node-sass could use fibers: true to avoid blocking on async importers. The fibers package does not build on Node 16+ and is unmaintained. Remove it; don't spend time making it work.
Extension points worth knowing (all migration debt): importer (resolve vendor/ partials), functions (inject runtime values), indentedSyntax (.sass files), precision (rounding digits — dart-sass dropped this option entirely). Anything you build here must be rewritten for dart-sass; budget for it rather than treating it as a permanent integration layer.
Verdict for planning. With the package archived and 8.4k stars reflecting historical reach rather than current health, the day-to-day cost is concentrated in install/CI friction and the LibSass feature ceiling. If the project is on Laravel 9+ or Vite, node-sass should already be gone — if it's still in package-lock.json, that's a strong signal a stale laravel-mix version is pinning you to Node 18 and blocking upgrades elsewhere. Schedule the toolchain bump; don't build new integration against it.
How can I help you explore Laravel packages today?