Fix: Sass Error - Undefined Variable
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 @useOr with older node-sass:
SassError: Undefined variable: "$primary-color"
on line 12 of src/components/button.scssThe 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 (
@useor@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 --versionshould print something like1.x.y compiled with dart2js. If you seenode-sassanywhere in your dependency tree (npm ls node-sass), assume that’s what your bundler is using and that@use/@forwardwill be silently ignored. - Minute 2 — Check whether you’re accessing through the right namespace.
@use 'config/colors'exposes variables ascolors.$primary, notconfig.$primaryand 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 namedvariables.scsswithout 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.scssshows every place the migrator would have to add a namespace. Each one is a place your current code is implicitly relying on global@importbehavior. - Minute 6 — Search for
!default. If the variable is declared with!default, it’s only set when no earlier value exists. A second@useof the same file withwith: ($variable: value)may have set it tonull, 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@usenamespacing. 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.scssWhen 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.scssFix 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-sassMigrate to Dart Sass:
npm uninstall node-sass
npm install sassAfter switching, you have two options:
- Keep using
@import(works but deprecated, will be removed in Dart Sass 3.0) - Migrate to
@use/@forward(recommended)
Use the official migration tool:
npx sass-migrator module style.scssPro Tip: The Sass Migrator tool handles most of the
@importto@useconversion automatically, including adding namespaces and converting variable references. Run it with--dry-runfirst 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-Colorand$primary-colorare different variables.Verify file extensions. Sass processes
.scssand.sassdifferently. Don’t mix syntaxes in@usewithout 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.cssto isolate whether the issue is with Sass itself or your build tool’s configuration.Inspect
node_modulesfor a duplicate Sass install. A transitive dependency can pin an old version ofsassor pull innode-sasseven when you removed it from yourpackage.json. Runnpm ls sass node-sassand dedupe withnpm dedupeor pin a resolution inpackage.json.Check for
.scss.liquidor 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 installyet won’t havesassinnode_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.jsonfor accidentalsass-embedded. Thesass-embeddedpackage is faster but behaves slightly differently for legacy@importfiles. 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 thecolorsnamespace and the second one fails. Useasaliases (@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@useit from every component.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: NativeWind Not Working — Styles Not Applying, Dark Mode Broken, or Metro Bundler Errors
How to fix NativeWind issues — Tailwind CSS for React Native setup, Metro bundler configuration, className prop, dark mode, responsive styles, and Expo integration.
Fix: Tamagui Not Working — Styles Not Applying, Compiler Errors, or Web/Native Mismatch
How to fix Tamagui UI kit issues — setup with Expo, theme tokens, styled components, animations, responsive props, media queries, and cross-platform rendering.
Fix: Mantine Not Working — Styles Not Loading, Theme Not Applying, or Components Broken After Upgrade
How to fix Mantine UI issues — MantineProvider setup, PostCSS configuration, theme customization, dark mode, form validation with useForm, and Next.js App Router integration.
Fix: View Transitions API Not Working — No Animation Between Pages, Cross-Document Transitions Failing, or Fallback Missing
How to fix View Transitions API issues — same-document transitions, cross-document MPA transitions, view-transition-name CSS, Next.js and Astro integration, custom animations, and browser support.