Organising Stylesheets with SCSS Partials: 10 Code-Along Examples

Learn SCSS partials by splitting a stylesheet apart. Ten copy-and-run examples covering the underscore convention, file boundaries, component partials, self-contained dependencies, naming, portability, and source maps.

Learn SCSS partials by splitting a stylesheet apart. Ten copy-and-run examples covering the underscore convention, file boundaries, component partials, self-contained dependencies, naming, portability, and source maps.

The SCSS partials article covers the move from one unmanageable stylesheet to many manageable ones: a partial is a file holding a piece of your styles, marked by a leading underscore, existing only to be pulled into something else. The claim that makes it worth doing is that nothing about the shipped CSS changes, and that claim is checkable rather than something to take on faith, so this workbook starts by proving it with a diff and keeps compiling from there. It works through the whole range: splitting a monolith, what the underscore actually controls, deciding where one partial ends and the next begins, partials that emit no CSS at all, a partial per component, partials that declare their own dependencies, naming, portability between projects, and finding which partial a rule came from once everything is one file again. The assembly step, the imports and load order that stitch these back together, has its own workbook; this one is about the files. The idea that ties it together arrives in the last example. Install the compiler once with npm install --save-dev sass and compile with npx sass main.scss main.css.

1. Splitting a monolith

The promise is that the source becomes navigable and the output does not change at all. Here is the same stylesheet twice, once as one file and once as four, with the compiler as the judge.

// before/main.scss, everything in one place
$brand: #1e2331;
$space: 16px;
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", sans-serif; }
.header { background: $brand; color: #fff; padding: $space; }
.card { background: #fff; border-radius: 8px; padding: $space * 1.5; }
// after/_variables.scss
$brand: #1e2331;
$space: 16px;
// after/_reset.scss
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", sans-serif; }
// after/_header.scss
.header { background: $brand; color: #fff; padding: $space; }
// after/_card.scss
.card { background: #fff; border-radius: 8px; padding: $space * 1.5; }
// after/main.scss
@import 'variables';
@import 'reset';
@import 'header';
@import 'card';
npx sass before/main.scss before/main.css --no-source-map
npx sass after/main.scss after/main.css --no-source-map
diff before/main.css after/main.css && echo "IDENTICAL"
IDENTICAL

Two source layouts, one byte-for-byte identical stylesheet, 232 bytes either way. That is the whole trade being made visible: partials are a change to how you work, not to what you ship, so nobody downstream can tell whether your styles came from one file or forty. Notice that the split fell along obvious lines, with values in one file, resets in another, and one file per section of the page, and that no rule had to be rewritten to move. If splitting a stylesheet requires editing the rules themselves, the boundaries are in the wrong place, which is what Example 3 is about. The @import here still works and is what most existing projects use; the modern @use appears from Example 6 onward.

2. The underscore, and what it controls

The leading underscore is the only thing that makes a file a partial, and what it controls is narrower than most people assume.

// src/_button.scss (underscore: a building block)
.btn { background: #1e2331; color: #fff; padding: 12px 20px; }
// src/card.scss (no underscore: a finished output)
.card { background: #fff; padding: 24px; }
// src/main.scss
@import 'button';
@import 'card';
npx sass src:dist --no-source-map
dist/
├── card.css
└── main.css
# but a partial is not forbidden, only skipped
npx sass src/_button.scss forced.css --no-source-map
/* forced.css */
.btn {
background: #1e2331;
color: #fff;
padding: 12px 20px;
}

Compiling the folder produced two stylesheets rather than one, because card.scss has no underscore and so the compiler treated it as an entry point in its own right, writing its rules to dist/card.css as well as into main.css_button.scss was skipped. The second command is the part worth knowing: naming the partial explicitly compiles it perfectly happily, which tells you the underscore is not a permission or a different kind of file, only a marker that says “do not pick this up when you are compiling everything in a directory”. That is useful in practice, since compiling a single component partial by hand is a quick way to check it in isolation, which Example 6 turns into a test.

3. One concern per file

Files split by convenience drift back into monoliths. The signal that a partial has outgrown itself is that its name no longer describes everything inside it.

// _components.scss, six months later
.btn { background: #1e2331; color: #fff; padding: 12px 20px; border-radius: 6px; }
.btn--ghost { background: transparent; color: #1e2331; }
.card { background: #fff; border-radius: 8px; padding: 24px; }
.card__title { margin: 0 0 8px; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 999px; }
.badge--new { background: #1e2331; color: #fff; }
// _button.scss
.btn { background: #1e2331; color: #fff; padding: 12px 20px; border-radius: 6px; }
.btn--ghost { background: transparent; color: #1e2331; }
// _card.scss
.card { background: #fff; border-radius: 8px; padding: 24px; }
.card__title { margin: 0 0 8px; }
// _badge.scss
.badge { display: inline-block; padding: 4px 10px; border-radius: 999px; }
.badge--new { background: #1e2331; color: #fff; }
// main.scss
@import 'button';
@import 'card';
@import 'badge';

One file that answered “where do I change the badge colour” with “somewhere in components” became three that answer it with a filename. Again nothing was rewritten, only moved, which is the test for a good boundary: if you can cut the file at a blank line between rules and both halves still make sense, the concerns were already separate and the file was just holding them in one place. The useful question is not how many lines a partial has but how many answers it gives, so a 300-line _card.scss that is entirely about cards is healthier than an 80-line _components.scss that is about three unrelated things. Splitting also buys you cleaner history, since two people editing the button and the card no longer touch the same file.

4. Partials that produce no CSS

Some partials define things and emit nothing. Recognising which is which is what makes load order obvious later.

// _variables.scss (emits nothing)
$brand: #1e2331;
$space: 16px;
// _mixins.scss (emits nothing)
@mixin surface { background: #fff; border-radius: 8px; }
// _card.scss (emits CSS, and consumes both files above)
.card { @include surface; padding: $space; color: $brand; }
// main.scss
@import 'variables';
@import 'mixins';
@import 'card';
/* main.css */
.card {
background: #fff;
border-radius: 8px;
padding: 16px;
color: #1e2331;
}

Three partials in, one rule out, and not a trace of the first two files in the output. Definitions are instructions to the compiler rather than CSS, so a variables partial you never use costs nothing at all, and a mixin only becomes bytes at the point something includes it. That split gives you a natural way to sort a project: the files that define things go at the top of your entry point because everything else depends on them, and the files that emit things follow in cascade order. It also means you can be generous with definition partials and should be careful with output ones, since fifty unused variables are free and fifty unused component rules are not.

5. A partial per component

The cleanest mapping in a component-based stylesheet is one partial per component, holding the block, its elements, and its modifiers.

// components/_card.scss
@use '../abstracts/variables' as v;
@use '../abstracts/mixins' as m;
// everything the card is, in the file called card
.card { @include m.surface; padding: v.$space * 1.5; }
.card__title { margin: 0 0 8px; color: v.$brand; }
.card__body { margin: 0; color: v.$muted; }
.card--featured { border-color: v.$brand; border-width: 2px; }
/* the card's contribution to main.css */
.card {
background: #fff;
border: 1px solid #e2e6ef;
border-radius: 8px;
padding: 24px;
}
.card__title {
margin: 0 0 8px;
color: #1e2331;
}
.card__body {
margin: 0;
color: #6f7580;
}
.card--featured {
border-color: #1e2331;
border-width: 2px;
}

Four selectors, one file, and a name that predicts all four. This is where partials and a naming convention reinforce each other, because BEM already says a block owns its elements and modifiers, and one partial per block turns that into something the filesystem enforces: if a rule does not start with card, it does not belong in _card.scss. The practical payoff is deletion. Retiring a component means deleting one file and one line from the entry point, with no hunting for stray rules that were filed elsewhere, which is the thing that never happens in a stylesheet organised by page or by when the rule was written.

6. Self-contained partials

A partial that depends on whoever loaded it is fragile. With @use, each file states what it needs, and you can prove it by compiling the partial on its own.

// _variables.scss
$brand: #1e2331;
$space: 16px;
// _card.scss
@use 'variables' as v; // this partial states its own dependency
.card {
background: #fff;
padding: v.$space;
color: v.$brand;
}
# compile the partial by itself, with no entry point involved
npx sass _card.scss card-alone.css --no-source-map
/* card-alone.css */
.card {
background: #fff;
padding: 16px;
color: #1e2331;
}
// _card-bad.scss, the same rule relying on its importer
.card { padding: $space; }
Error: Undefined variable.
1 │ .card { padding: $space; }
│ ^^^^^^
_card-bad.scss 1:18 root stylesheet

One partial compiles alone and the other cannot, which is a test you can run on any file in a project. Under the old @importboth versions work as long as the entry point loads the variables first, and that is precisely the weakness: the file appears fine while carrying an invisible requirement that only breaks when someone reorders the imports or reuses the component elsewhere. @use makes the dependency explicit at the top of the file where a reader sees it, namespaces it so v.$brand says where the value came from, and turns a runtime-order problem into a compile error. The rule that follows is short: a partial should compile on its own, and if it cannot, it is missing a @use.

7. Naming, and reading a folder as a table of contents

The file listing is documentation whether you intend it or not. These two projects contain identical CSS.

styles/
├── _styles2.scss
├── _new.scss
├── _stuff.scss
├── _temp-fix.scss
└── main.scss
styles/
├── _variables.scss
├── _reset.scss
├── _button.scss
├── _card.scss
└── main.scss
// main.scss reads as an index of the project
@use 'variables';
@use 'reset';
@use 'button';
@use 'card';

The second listing answers “where do I change the button padding” without anyone opening a file, and the first guarantees that somebody opens all four. Two habits produce the difference. Name a partial after what it holds rather than when you added it or why, so _button.scss rather than _new.scss and never _temp-fix.scss, which outlives every project it appears in. And keep the entry point’s list in a deliberate order, because a newcomer reads it top to bottom as a map of the stylesheet, so grouping definitions, then base styles, then components tells them how the project is built before they have read a single rule. A folder is the cheapest documentation you will ever write and the only kind that cannot go out of date.

8. Partials you carry between projects

The best test of a partial’s boundaries is whether it survives being dropped into a different project unchanged.

// _reset.scss, portable: depends on nothing
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
img, video { max-width: 100%; display: block; }
// _a11y.scss, portable: no project-specific values
@mixin visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
// _card.scss, NOT portable: bound to this project's variables
@use 'variables' as v;
.card { background: #fff; padding: v.$space; color: v.$brand; }
// the new project's main.scss
@use 'reset'; // copied in, unchanged
@use 'a11y'; // copied in, unchanged
@use 'card'; // needs a _variables.scss to come with it

Two files that move without edits and one that brings luggage. That distinction is worth noticing rather than fixing, because a card component should be tied to your design tokens, and the point is knowing which of your partials are genuinely reusable so you can keep them together and stop rewriting them every project. The portable ones share a property: they depend on nothing outside themselves, which is Example 6’s test again from a different angle. Over a few projects this becomes a small personal library of resets, accessibility helpers, and mixins that you paste in on day one, and the only thing that keeps it usable is that each file does one nameable job.

9. Which partial did this rule come from

Once everything compiles into one file, the browser shows you main.css line 340 and you still have to find the source. Source maps solve it, and Dart Sass writes them by default.

// _header.scss
.header { background: #1e2331; color: #fff; }
// _card.scss
.card {
background: #fff;
padding: 24px;
}
// main.scss
@use 'header';
@use 'card';
npx sass main.scss main.css
main.css
main.css.map
/* the last line of main.css */
/*# sourceMappingURL=main.css.map */
sources listed in main.css.map:
["_header.scss", "_card.scss"]

A second file next to the CSS, and a comment at the bottom pointing the browser at it. That map is what makes the Styles panel in dev tools say _card.scss:2 instead of main.css:14, so inspecting an element takes you to the partial you would actually edit. It is on by default, which is why --no-source-map appears in most of the commands in this workbook to keep the output clean, and it is worth leaving on in development and off in production builds. This is the piece that makes splitting into forty files practical rather than annoying, because without it the cost of partials is paid every time you debug, and with it the browser knows your source layout as well as you do.

10. The entry point, and the model

Put it together and a project is a shallow tree of small files with one file at the top that is the only thing you compile.

src/
├── abstracts/
│ ├── _variables.scss
│ └── _mixins.scss
├── base/
│ └── _reset.scss
├── components/
│ ├── _card.scss
│ └── _index.scss
└── main.scss
// components/_index.scss
@forward 'card';
// main.scss, the only file compiled
@use 'base/reset';
@use 'components';
/* main.css */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Segoe UI", sans-serif;
background: #f7f9fc;
}
.card {
background: #fff;
border: 1px solid #e2e6ef;
border-radius: 8px;
padding: 24px;
}
.card__title {
margin: 0 0 8px;
color: #1e2331;
}
.card__body {
margin: 0;
color: #6f7580;
}
.card--featured {
border-color: #1e2331;
border-width: 2px;
}

Six source files, one stylesheet, and an entry point of two lines because the components folder presents itself through an index. Note that abstracts is nowhere in that entry point, since _card.scss declares its own need for the variables and mixins, which is Example 6 scaled up: the tree describes where files live, and each file describes what it needs.

The model underneath is that a partial is a unit of editing, not a unit of delivery, and every rule in this workbook follows from that one sentence. The underscore exists to keep those two units from being confused, marking a file as something you work in rather than something you ship. The boundaries between partials should be drawn around what changes together, since the only thing you gain by splitting is knowing which file to open, and a name that does not predict its contents gives that back. Dependencies belong to the file that has them, not to the entry point, because a partial that cannot compile alone is a unit of editing that only works in one arrangement. And the compiled output stays a single file regardless, so none of these decisions cost your users anything, which is why the right number of partials is however many make the project navigable and not one fewer.

Work through these and you have the article in practice: a monolith split into four files with a diff proving the output identical; the underscore and the narrow thing it controls; boundaries drawn where a file stops answering one question; definition partials that emit no CSS; one partial per component, matching the block it styles; self-contained partials proved by compiling them alone; names and orderings that turn a folder into a table of contents; the partials portable enough to carry between projects; source maps that point the browser back at the file you would edit; and an entry point plus the editing-versus-delivery model that explains the rest. The habit that follows is a filing one: when you write a new rule, ask which file’s name already describes it, and if the answer is none of them, that is a new partial rather than a line appended to the nearest one.

Thanks for reading, Andrei.

View Comments (2)

Leave a Reply

Subscribe to My Newsletter

Subscribe to my email newsletter to get the latest posts delivered right to your email. Pure inspiration, zero spam.

Discover more from Discuss Data Science, Machine Learning and Analytics

Subscribe now to keep reading and get access to the full archive.

Continue reading