Learn SCSS imports by building them. Ten copy-and-run examples covering partial naming, import order, a single entry point, default overrides, duplicate imports, folder index files, and @use.
The SCSS imports article covers the assembly step in a modular stylesheet: splitting your styles into partials only helps if you have a clean way to combine them again, and in SCSS that job belongs to the import system. Get it wrong and variables come back undefined, rules land in the wrong order, and the same block of CSS ships to the browser twice. This workbook works through the whole range: your first import, what the compiler actually inlines, the underscore convention, why order decides everything, a single entry point across four layers, overriding a partial’s defaults, the duplicate-import trap, folder index files, and the modern replacement. The idea that ties it together arrives in the last example, and it explains why every rule above it exists.
1. Your first import
An import pulls the contents of one file into another, and that single mechanism is what makes modular stylesheets possible. You need the Dart Sass compiler for these examples, so install it once in the project folder with npm install --save-dev sass.
// _variables.scss$brand: #1e2331;$ink: #6f7580;$font-stack: "Segoe UI", sans-serif;
// main.scss@import 'variables'; // pulls in _variables.scssbody { font-family: $font-stack; color: $ink; background: #f7f9fc;}h1 { color: $brand; }
npx sass main.scss main.css
/* main.css */body { font-family: "Segoe UI", sans-serif; color: #6f7580; background: #f7f9fc;}h1 { color: #1e2331;}
Two source files, one stylesheet out. The first thing to notice is what you did not type: the import says 'variables', not '_variables.scss', because the compiler adds back both the leading underscore and the extension when it goes looking. The second is that no trace of the variables file survives into the output, since definitions are instructions to the compiler rather than CSS. You will also find a main.css.map sitting next to the result, which Dart Sass writes by default so browser dev tools can point you at the original partial instead of the compiled line; add --no-source-map if you would rather not have it. Compiling this will print a deprecation warning about @import, which Example 10 deals with properly.
2. What the compiler actually inlines
@import is closer to paste than to link. The clearest way to see that is to put a rule between two imports and watch where it lands in the output.
// _header.scss.site-header { background: #1e2331; color: #fff; padding: 16px 24px;}
// _footer.scss.site-footer { background: #6f7580; color: #fff; padding: 24px;}
// main.scss@import 'header';body { margin: 0; font-family: "Segoe UI", sans-serif; }@import 'footer';
/* main.css */.site-header { background: #1e2331; color: #fff; padding: 16px 24px;}body { margin: 0; font-family: "Segoe UI", sans-serif;}.site-footer { background: #6f7580; color: #fff; padding: 24px;}
The header rules, then the body rule, then the footer rules, in exactly the order the file reads top to bottom. Each import was replaced in place by the contents of the file it named, which is the whole mechanism and the reason everything else in this workbook follows. It also explains the delivery benefit: your source is split into as many files as you like, but the browser receives one stylesheet and makes one request, so the organisation costs nothing at runtime. Hold on to the mental image of inlining, because the next three examples are all consequences of it.
3. Why partials start with an underscore
The underscore is not decoration. It tells the compiler that a file is meant to be imported rather than compiled into a stylesheet of its own.
// src/_buttons.scss (partial: never compiled on its own).btn { background: #1e2331; color: #fff; padding: 12px 20px; }
// src/cards.scss (no underscore: gets its own output file).card { background: #fff; padding: 24px; }
// src/main.scss@import 'buttons';@import 'cards';
npx sass src:dist
dist/├── cards.css└── main.css
Compiling the whole folder produced two stylesheets when you wanted one. _buttons.scss behaved itself and appeared only inside main.css, while cards.scss was treated as an entry point in its own right and written out to dist/cards.css as well, so its rules now exist in two files. Nothing is broken, but you have a stray stylesheet that someone will eventually link by accident and a second copy of every card rule to keep in sync. The rule that avoids all of it is mechanical: if a file is not something you compile directly, give it an underscore. Note that the import statement never changes, since @import 'cards'finds either spelling.
4. Import order is not cosmetic
Because imports are inlined where they sit, a rule can only use a definition that arrived above it. This example gets that wrong on purpose.
// _variables.scss$brand: #1e2331;
// main.scss (broken: the rule runs before the definition arrives).badge { background: $brand;}@import 'variables';
Error: Undefined variable. ╷3 │ background: $brand; │ ^^^^^^ ╵ main.scss 3:15 root stylesheet
No CSS at all, just a compiler that has never heard of $brand. At the moment line 3 is evaluated the import on line 6 has not happened yet, so the variable genuinely does not exist, and moving that import to the top of the file is the entire fix. This is why the conventional structure loads configuration and tools before anything that could possibly use them, and it is worth reading the error carefully when it appears, because “undefined variable” in SCSS almost always means “defined, but too late”. Sass points at the line that used the name rather than the import that was misplaced, so the fix is usually a few lines above where the arrow lands.
5. When two partials style the same thing
Order does not only decide whether the build succeeds. It also decides which rule wins when two partials touch the same selector.
// _buttons.scss.btn { background: #1e2331; border-radius: 6px;}
// _theme.scss.btn { background: #6f7580;}
// main.scss@import 'buttons';@import 'theme';
/* main.css */.btn { background: #1e2331; border-radius: 6px;}.btn { background: #6f7580;}
Two separate .btn blocks in the output, not one merged rule, and buttons that render grey. SCSS did not resolve the conflict for you, it simply wrote both rules out in import order and left the normal CSS cascade to settle it, so the later one wins at equal specificity. That is a tool once you expect it: a theme partial imported last can adjust a component without touching the component’s own file. It is also the reason import order in your entry point should read from general to specific, because if _theme.scss were imported first its override would silently lose and you would be hunting a specificity bug that is really an ordering bug.
6. One entry point, four layers
The architectural decision that matters most is having a single file that imports everything and is the only file you compile. Grouping those imports into layers gives you a readable index of the entire stylesheet.
src/├── abstracts/│ ├── _variables.scss│ └── _mixins.scss├── base/│ └── _reset.scss├── layout/│ └── _container.scss├── components/│ └── _card.scss└── main.scss
// abstracts/_variables.scss$brand: #1e2331;$muted: #6f7580;$gap: 12px;
// abstracts/_mixins.scss@mixin surface($bg) { background: $bg; color: #fff; padding: $gap * 2;}
// base/_reset.scss* { box-sizing: border-box; }body { margin: 0; font-family: "Segoe UI", sans-serif; }
// layout/_container.scss.container { max-width: 960px; margin: 0 auto; padding: $gap * 2;}
// components/_card.scss.card { @include surface($brand); border-radius: 6px;}
// main.scss (the only file you compile)// 1. Abstracts: definitions only, no CSS of their own@import 'abstracts/variables';@import 'abstracts/mixins';// 2. Base: resets and element defaults@import 'base/reset';// 3. Layout: structural scaffolding@import 'layout/container';// 4. Components: the discrete UI pieces@import 'components/card';
/* main.css */* { box-sizing: border-box;}body { margin: 0; font-family: "Segoe UI", sans-serif;}.container { max-width: 960px; margin: 0 auto; padding: 24px;}.card { background: #1e2331; color: #fff; padding: 24px; border-radius: 6px;}
Six source files, one stylesheet, and the abstracts layer contributing not a single line of it. That is the point of putting it first: variables and mixins emit nothing on their own but everything below depends on them, so $gap * 2 resolves to 24px in the layout file and @include surface($brand) expands inside the card. The output then arrives in cascade order for free, with resets before structure and structure before components. Paths are relative to the importing file, which is why nested folders need 'abstracts/variables' while a partial inside a folder can import its neighbour by name alone. Glancing at this entry point tells a new developer how the project is organised without opening anything else.
7. Overriding a partial’s defaults
A partial that declares its variables with !default is offering them for override, and import order is what lets you accept.
// _theme-defaults.scss$brand: #1e2331 !default; // "use this unless it is already set"$radius: 6px !default;
// _button.scss.btn { background: $brand; border-radius: $radius; color: #fff; padding: 12px 20px;}
// main.scss$brand: #6f7580; // set BEFORE the defaults are imported@import 'theme-defaults'; // !default sees $brand already exists, leaves it alone@import 'button';
/* main.css */.btn { background: #6f7580; border-radius: 6px; color: #fff; padding: 12px 20px;}
Your grey brand colour won and the default radius came through untouched, without either partial being edited. !defaultmeans “assign this only if the variable has no value yet”, so setting $brand above the import is what makes the override stick, and moving that line below the import would silently do nothing. This is exactly how you configure a third-party library such as Bootstrap, and it is the one case where a rule deliberately comes before its import. Everything with !defaultalso has to be genuinely optional, since a partial cannot see what a later file will do.
8. The double import that duplicates your CSS
The temptation with @import is to pull a partial in wherever it seems needed, including from inside another partial. Here is what that costs.
// _card.scss.card { background: #1e2331; color: #fff; padding: 24px;}
// _components.scss@import 'card'; // the mistake: a partial importing another partial.card--wide { max-width: 720px; }
// main.scss@import 'card'; // and main imports it too@import 'components';
/* main.css */.card { background: #1e2331; color: #fff; padding: 24px;}.card { background: #1e2331; color: #fff; padding: 24px;}.card--wide { max-width: 720px;}
The card rule is in there twice, because @import inlines on every encounter and has no memory of files it has already seen. On a small example that is wasted bytes, but scale it to a colour scheme or a set of mixins that also emit CSS and you get a stylesheet noticeably larger than it should be, plus override bugs that make no sense until you read the compiled file. Worse, the duplicate lands in a different position each time, so a rule you thought was safely early reappears late and starts winning the cascade. The fix is the discipline from Example 6: partials define styles, one entry point assembles them, and no partial imports another.
9. Giving a folder its own front door
Listing thirty component partials in your entry point gets unwieldy. A folder can present a single file that gathers its own contents.
// components/_card.scss.card { background: #1e2331; color: #fff; padding: 24px; }
// components/_button.scss.btn { background: #6f7580; color: #fff; padding: 12px 20px; }
// components/_index.scss (the folder's own front door)@import 'card';@import 'button';
// main.scss@import 'components'; // resolves to components/_index.scss
/* main.css */.card { background: #1e2331; color: #fff; padding: 24px;}.btn { background: #6f7580; color: #fff; padding: 12px 20px;}
One import in the entry point, both components in the output. Naming a directory sends the compiler looking for _index.scss inside it, so the folder owns the list of what it contains and adding a component means editing the index next to it rather than a file three levels up. This is the one sanctioned exception to Example 8’s no-partial-imports-another rule, and it survives the exception because an index file assembles rather than defines, and each component is still named exactly once. The cost is that your entry point no longer shows the full inventory, which is a fair trade past about a dozen files but not before.
10. From @import to @use and @forward
Every problem above comes from the same source: @import pastes text into a global scope. The modern rules replace pasting with loading, and Sass has deprecated @import in favour of them.
// _variables.scss$brand: #1e2331;$muted: #6f7580;
// _reset.scss (this one emits real CSS)* { box-sizing: border-box; }
// components/_card.scss@use '../variables' as v; // namespaced, not global@use '../reset'; // loaded here as well as in main.scss.card { background: v.$brand; color: #fff; padding: 24px;}
// components/_button.scss@use '../variables' as v;.btn { background: v.$muted; color: #fff; padding: 12px 20px;}
// components/_index.scss@forward 'card';@forward 'button';
// main.scss@use 'reset'; // also loaded by components/_card.scss@use 'components'; // resolves to components/_index.scss
/* main.css */* { box-sizing: border-box;}.card { background: #1e2331; color: #fff; padding: 24px;}.btn { background: #6f7580; color: #fff; padding: 12px 20px;}
The reset appears once even though two files loaded it, and Example 8’s duplication problem stops being something you have to remember. Three things changed. Every file now declares its own dependencies, so components/_card.scss states that it needs the variables instead of hoping the entry point loaded them first. Members arrive under a namespace, so v.$brandsays where the value came from and two partials can define $brand without colliding. And a file is loaded once per compilation no matter how many times it is used. @forward is the index-file half of the pair, re-exporting what a folder contains so @use 'components' reaches everything inside.
The model underneath both directives is a pair of questions, and reading the property that way explains all ten examples. The first is where does this code come from, which @import answers with “somewhere above, in one shared global pile” and @use answers with “this specific file, under this name”. The second is when does it arrive, which under @import is wherever the statement sits, and under @use is once, before your rules run. Almost every import bug is one of those two answers being different from what you assumed: an undefined variable is the first question, a duplicated rule or a lost override is the second.
Work through these and you have the article in practice: a first import and the naming the compiler resolves for you; inlining and what it means for output order; the underscore that keeps a partial from compiling on its own; order as a build error and then as a cascade decision; a single entry point across four layers; !default overrides that must come first; the duplicate import that quietly doubles your CSS; index files for folders that have grown; and the move to @use and @forwardwith the loading model that explains the rest. The habit that follows is a simple one: when a variable is undefined, a rule appears twice, or an override refuses to take, open the compiled CSS and read it top to bottom, because the answer is almost always sitting in the order.
Thanks for reading, Andrei.
[…] Assembling a Stylesheet with SCSS Imports: 10 Code-Along Examples […]
[…] it: work through Assembling a Stylesheet with SCSS Imports: 10 Code-Along Examples and assemble each stylesheet yourself. Free […]