Skip to content

Fix: Sass Error - Undefined Variable

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

Fix the Sass undefined variable error caused by @use vs @import migration, variable scope issues, and dart-sass namespace changes with clear solutions.

The Error

You compile your Sass files and get:

Error: Undefined variable.
   |
12 | color: $primary-color;
   |        ^^^^^^^^^^^^^^
  src/components/button.scss 12:8  @use

Or with older node-sass:

SassError: Undefined variable: "$primary-color"
        on line 12 of src/components/button.scss

The variable exists in another file, but Sass can’t find it during compilation.

Why This Happens

Sass variable visibility depends on how you import files and which Sass implementation is compiling them. The modern @use rule introduced by Dart Sass creates a namespaced module for every file you load. Variables declared inside that file are not added to the global scope — they live behind the module’s namespace and must be accessed with namespace.$variable. The legacy @import rule did the opposite: it copied every declaration into the global scope of the consuming file, so any $variable defined anywhere was reachable from anywhere. This is the single most common cause of the “Undefined variable” error when migrating an older codebase.

There’s a second cause that looks identical at the error surface but has a completely different fix: the Sass implementation itself. Dart Sass and the deprecated LibSass / node-sass produce different error messages, support different syntax, and resolve files in subtly different ways. node-sass never implemented @use or @forward, so if you have a partial migration where some files use the new syntax and the wrong compiler is wired into your build chain, the modules silently never load and every namespaced reference fails. Worse, sass-loader picks up whichever package is installed first, so a transitive dependency can pin you to an outdated implementation without you noticing.

A third class of failure is purely about file resolution. Sass partials must start with an underscore but be referenced without it. Dart Sass also resolves @use 'foo' to either foo.scss, _foo.scss, foo/_index.scss, or _foo/_index.scss — and if your file is in a non-standard location, the compiler may load a stale file with the same name from a load path you forgot was configured. The error “Undefined variable” then comes not from a typo but from Sass having loaded a different file than you expected.

Diagnostic Timeline

Run this sequence in order. Most “Undefined variable” errors are resolved at minute 2.

  • Minute 0 — Read the full error message, not just the variable name. Dart Sass prints the file path and the directive (@use or @import) that triggered the failure. If the directive line shows @use, the variable is unreachable because of namespacing, not because it’s missing. If it shows @import, you’re in the legacy path and the file probably isn’t being loaded at all.
  • Minute 1 — Confirm the compiler. npx sass --version should print something like 1.x.y compiled with dart2js. If you see node-sass anywhere in your dependency tree (npm ls node-sass), assume that’s what your bundler is using and that @use/@forward will be silently ignored.
  • Minute 2 — Check whether you’re accessing through the right namespace. @use 'config/colors' exposes variables as colors.$primary, not config.$primary and not bare $primary. The namespace is always the last path segment.
  • Minute 3 — Verify the file actually resolves. Run the failing file through the Sass CLI directly: npx sass src/components/button.scss /tmp/out.css. If the CLI succeeds but your build fails, the bundler’s load paths differ from the CLI defaults. If the CLI also fails, the error is real.
  • Minute 4 — Check the partial filename. @use 'variables' expects _variables.scss. A file named variables.scss without the underscore is treated as an entry point, not a partial, and is silently skipped during resolution in some versions.
  • Minute 5 — Run sass-migrator with --dry-run. npx sass-migrator module --dry-run src/main.scss shows every place the migrator would have to add a namespace. Each one is a place your current code is implicitly relying on global @import behavior.
  • Minute 6 — Search for !default. If the variable is declared with !default, it’s only set when no earlier value exists. A second @use of the same file with with: ($variable: value) may have set it to null, which counts as defined but produces the same downstream error in different contexts.

Fix 1: Use the Correct Namespace

When you @use a file, its variables are available under a namespace matching the filename:

// _variables.scss
$primary-color: #3b82f6;
$font-size-base: 16px;

// button.scss
@use 'variables';

.button {
  // Wrong - no namespace
  color: $primary-color;

  // Correct - with namespace
  color: variables.$primary-color;
  font-size: variables.$font-size-base;
}

The namespace is the last component of the file path without the extension. For @use 'config/variables', the namespace is variables.

Customize the namespace with as:

@use 'variables' as vars;

.button {
  color: vars.$primary-color;
}

Or remove the namespace entirely with as *:

@use 'variables' as *;

.button {
  color: $primary-color; // Works without namespace
}

Common Mistake: Overusing as * defeats the purpose of @use namespacing. Reserve it for files you use extensively (like your design tokens). For everything else, keep the namespace to avoid collisions.

Fix 2: Configure @forward for Library Files

If you have an index file that re-exports variables from multiple files, use @forward:

// abstracts/_variables.scss
$primary-color: #3b82f6;

// abstracts/_mixins.scss
@mixin flex-center { display: flex; align-items: center; justify-content: center; }

// abstracts/_index.scss
@forward 'variables';
@forward 'mixins';

// components/button.scss
@use '../abstracts';

.button {
  color: abstracts.$primary-color;
  @include abstracts.flex-center;
}

@forward makes the forwarded module’s members available to anyone who @uses the forwarding file. Without @forward, @use 'abstracts' would only expose members defined directly in _index.scss.

Fix 3: Fix File Import Order

Variables must be defined before they’re used. With @use, the order matters:

// Wrong - using variable before it's available
@use 'components/button'; // Uses $primary-color
@use 'variables'; // Defines $primary-color

// Correct - define first, use second
@use 'variables';
@use 'components/button';

However, each file should @use its own dependencies rather than relying on import order in a main file:

// components/_button.scss — self-contained
@use '../variables';

.button {
  color: variables.$primary-color;
}

This makes each file independent and eliminates order-related issues.

Fix 4: Fix Partial File Naming

Sass partials must start with an underscore. Without it, Sass may not find the file:

styles/
├── _variables.scss    Partial (imported, not compiled alone)
├── variables.scss     Compiled as standalone file
└── main.scss

When you write @use 'variables', Sass looks for _variables.scss. If the file is named variables.scss without the underscore, Sass treats it as a standalone file, not an importable partial.

Rename your files to include the underscore prefix:

mv variables.scss _variables.scss
mv mixins.scss _mixins.scss

Fix 5: Migrate from node-sass to Dart Sass

node-sass is deprecated and doesn’t support @use or @forward. If you’re using @import with node-sass, everything is global. But if you switch to Dart Sass without updating your import style, variables break:

# Check which Sass you're using
npx sass --version  # Dart Sass
node -e "console.log(require('node-sass').info)"  # node-sass

Migrate to Dart Sass:

npm uninstall node-sass
npm install sass

After switching, you have two options:

  1. Keep using @import (works but deprecated, will be removed in Dart Sass 3.0)
  2. Migrate to @use/@forward (recommended)

Use the official migration tool:

npx sass-migrator module style.scss

Pro Tip: The Sass Migrator tool handles most of the @import to @use conversion automatically, including adding namespaces and converting variable references. Run it with --dry-run first to preview changes.

Fix 6: Fix Load Paths

If your Sass files are in a non-standard directory, Sass may not find them. Configure load paths:

# CLI
sass --load-path=src/styles main.scss output.css

# Webpack (sass-loader)
{
  loader: 'sass-loader',
  options: {
    sassOptions: {
      includePaths: [path.resolve(__dirname, 'src/styles')]
    }
  }
}

For Vite:

// vite.config.js
export default {
  css: {
    preprocessorOptions: {
      scss: {
        includePaths: ['src/styles']
      }
    }
  }
}

With load paths configured, you can use shorter imports:

// Instead of
@use '../../styles/variables';

// You can write
@use 'variables';

Fix 7: Fix Variable Scope in Control Structures

Variables defined inside control structures (@if, @each, @for) are local to that block:

// Wrong - $color is scoped to the @if block
@if $theme == 'dark' {
  $color: #ffffff;
}
.text { color: $color; } // Error: Undefined variable

// Correct - declare outside, assign inside
$color: #000000;
@if $theme == 'dark' {
  $color: #ffffff !global;
}
.text { color: $color; }

The !global flag is needed to modify a variable from an outer scope inside a control structure. Without it, Sass creates a new local variable.

For mixin arguments, variables are scoped to the mixin body:

@mixin theme($bg, $text) {
  background: $bg;
  color: $text;
  // $bg and $text don't exist outside this mixin
}

Fix 8: Configure Global Variables with @forward ... with

Sometimes you need to override default variables from a library. Use @forward ... with to set configurable defaults:

// _config.scss
$primary-color: #3b82f6 !default;
$border-radius: 4px !default;

// _index.scss
@forward 'config';

// main.scss — override defaults
@use 'index' with (
  $primary-color: #ef4444,
  $border-radius: 8px
);

Variables must be marked with !default to be configurable. The with clause only works with @use, not @forward.

For third-party libraries like Bootstrap:

// Override Bootstrap variables before importing
@use 'bootstrap/scss/bootstrap' with (
  $primary: #custom-color,
  $enable-shadows: true
);

Still Not Working?

  • Check for typos in variable names. Sass variables are case-sensitive. $Primary-Color and $primary-color are different variables.

  • Verify file extensions. Sass processes .scss and .sass differently. Don’t mix syntaxes in @use without specifying the extension.

  • Clear your build cache. Stale cached files can cause phantom errors. Delete .sass-cache/, node_modules/.cache/, and your output directory.

  • Check for circular imports. If file A uses file B and file B uses file A, Sass may fail silently. Restructure to break the cycle — extract shared variables into a separate file.

  • Verify your bundler configuration. Webpack, Vite, and other bundlers resolve Sass imports differently. Check that your sass-loader or preprocessor options match your file structure.

  • Test with the Sass CLI directly. Run npx sass input.scss output.css to isolate whether the issue is with Sass itself or your build tool’s configuration.

  • Inspect node_modules for a duplicate Sass install. A transitive dependency can pin an old version of sass or pull in node-sass even when you removed it from your package.json. Run npm ls sass node-sass and dedupe with npm dedupe or pin a resolution in package.json.

  • Check for .scss.liquid or templated extensions. Build systems like Jekyll and Shopify use templated Sass extensions that are processed before Sass runs. Variables interpolated through the template can vanish silently, leaving the Sass compiler with an empty reference that reports as undefined.

  • Look for missing modules on first install. A fresh clone that hasn’t run npm install yet won’t have sass in node_modules, and many build tools fall back to a different preprocessor or skip Sass entirely without warning. The error then shows up as undefined variables across the entire project rather than as a clear “module not found.”

  • Look at package-lock.json for accidental sass-embedded. The sass-embedded package is faster but behaves slightly differently for legacy @import files. If a teammate added it, your local builds and CI builds may resolve variables differently.

  • Disable parse caching in your editor. VS Code and other editors keep their own Sass language server cache. After a refactor, the inline diagnostics can lag behind the actual compilation result by several seconds. Re-run the build from a terminal to get the truth.

  • Watch out for shadowed module names. If you @use 'colors' and also @use 'theme/colors', both modules try to claim the colors namespace and the second one fails. Use as aliases (@use 'theme/colors' as theme-colors) to keep them distinct. The error message in this case still says “Undefined variable” rather than “namespace collision” because the second module is loaded but its variables can’t be reached by name.

  • Confirm the file isn’t being processed twice. Some build setups (Astro’s CSS pipeline, Vue SFC <style lang="scss">) compile each component’s style block in isolation. A variable defined in a parent layout’s <style> isn’t reachable from a child component’s <style> even though both appear to be in the same file. The fix is to extract shared variables into a partial and @use it from every component.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles