Why Your Sass @import Chains Break — and How @use Fixes the Namespace Problem
Sass's @use and @forward replace the global-namespace chaos of @import with explicit, scoped dependencies. Here's how the module system works, with a migration path that won't break your build.
31 Aug 2026, 12:01 UTC

If you've maintained a large Sass codebase for any length of time, you've hit the failure mode: two partials both define a $spacing variable, an @import order change silently alters your theme colors, and a shared mixin gets compiled into the output three times because three files imported it. The root cause is that @import dumps everything into one global namespace. Sass's module system — @use and @forward — was designed specifically to end that.
The thesis is simple: make dependencies explicit and scoped, and most of the spooky-action-at-a-distance in big stylesheets disappears.
What @import actually does wrong
@import is textual inclusion with global scope. Every variable, mixin, and function from every imported file becomes visible everywhere downstream. That creates three recurring problems:
- Name collisions. Two libraries defining
$gutteror aclearfixmixin silently overwrite each other depending on import order. - Duplicate output. Import a file containing actual CSS rules from five places, and those rules appear five times in the compiled stylesheet.
- Hidden dependencies. A partial can use a variable it never imported, because some earlier file happened to pull it in. Reorder the imports and the build breaks with an undefined-variable error in a file you didn't touch.
The !default flag was the old workaround for configuration, but it only papers over the problem — you're still coordinating through global mutable state.
How @use changes the rules
@use loads a module once, no matter how many files reference it, and puts its members behind a namespace. A mixin named button in _forms.scss is only reachable as forms.button. Two modules can both define $gutter with zero conflict, because each lives in its own namespace.
Configuration also becomes explicit. Instead of setting a global variable before an import and hoping the order is right, you pass configuration at the point of use:
// Run in your project root with Dart Sass installed (sass --version to check).
// Requires Dart Sass 1.23+; LibSass does not support the module system.
// _theme.scss
$primary: blue !default;
// main.scss
@use "theme" with (
$primary: rebeccapurple
);
@use "forms";
.login {
@include forms.button;
color: theme.$primary;
}Compile with sass main.scss out.css (no special permissions needed; it writes one CSS file). The check: forms.button's rules appear exactly once in out.css, even if _forms.scss is also used by other modules in the graph. If you see duplicated rule blocks, something in the chain still uses @import.
@forward: building a public API
@use alone would force consumers to know your internal file layout. @forward solves this: an entry-point file re-exports selected members so consumers write one @use line.
// _index.scss — the public entry point
@forward "theme";
@forward "forms" show button, input-base;
@forward "layout" hide $internal-debug-flag;The show and hide clauses are the important part. Internal helpers stay internal; consumers see only the API you chose to expose. This is the maintainability win that @import could never offer — you can refactor internals freely as long as the forwarded surface stays stable.
The trade-offs worth knowing
The module system is not free. Three honest costs:
- Migration is all-or-nothing per dependency chain. A file loaded via
@usecan't see globals defined by a consumer's earlier@import, so legacy code that relied on ambient variables breaks until every reference is updated. Sass provides asass-migratortool that automates much of this, but plan for manual fixes around shared configuration. - Implementation support. Only Dart Sass fully supports
@use/@forward. If your build pipeline still runs LibSass (older node-sass setups), the module system simply won't compile — verify withsass --versionbefore committing to the migration. - Namespace verbosity. Prefixing everything (
forms.button,layout.container) can get noisy. The@use "forms" as *escape hatch removes the namespace, but that reintroduces collision risk — use it sparingly, for one utility module at most.
Where to start
Don't rewrite the whole codebase. Pick one leaf module — a partial that nothing else imports — convert it to a module, and update its consumers to @use. Compile and diff the output CSS: it should be identical or smaller, never larger. Then work inward toward your entry point. By the time you convert the root file, the global namespace is empty, and the dependency graph you can read in the @use lines is the one that actually exists.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.