DevToys Web Pro iconDevToys Web ProBlog
Prevedeno z LocalePack logoLocalePack
Ocenite nas:
Preizkusite razširitev brskalnika:
← Back to Blog

SCSS Formatter Guide: Prettier, Stylelint, Nesting, and Pre-commit

8 min read

SCSS sits at the intersection of CSS and a programming language — it has variables, loops, mixins, and modules. That power makes consistent formatting more important, not less. Poorly nested SCSS is harder to debug than unformatted plain CSS. Use the SCSS Formatter to clean up stylesheets directly in your browser, then set up automation with the toolchain described here.

Related reading: Code Formatters Guide for the broader picture and Python Formatter Guide for a comparable deep-dive in another language.

SCSS vs Sass: Which Syntax Is Current

The Sass preprocessor ships two syntaxes. SCSS (Sassy CSS) uses curly braces and semicolons — it is a superset of CSS, so any valid CSS file is also valid SCSS. The indented syntax (the original .sass extension) uses significant whitespace instead of braces, similar to Python or Pug.

FeatureSCSS (.scss)Indented (.sass)
Braces and semicolonsRequiredOmitted
Valid CSS as-isYesNo
Prettier supportFullPartial
Ecosystem toolingDominantNiche
Recommended for new projectsYesOnly if team preference

The Sass team considers SCSS the primary syntax. Unless your team has a strong existing preference for indented style, choose SCSS. In mixed-format codebases — where legacy .sass files coexist with new .scss — configure Prettier separately per extension using overrides in .prettierrc.

Prettier for SCSS: Default Rules and Key Options

Prettier treats SCSS as a first-class language. Run prettier --write "**/*.scss" and it enforces consistent quotes, spacing around operators, trailing semicolons, and brace placement without configuration. The defaults that matter most for stylesheets:

  • singleQuote — defaults to false (double quotes). Set to true to use single quotes in string values.
  • printWidth — defaults to 80. Long selector chains and @include calls can exceed this; Prettier wraps them at property boundaries, not mid-selector.
  • tabWidth / useTabs — standard indentation settings, applied consistently inside nested blocks.
// .prettierrc
{
  "singleQuote": true,
  "printWidth": 100,
  "overrides": [
    {
      "files": "*.scss",
      "options": {
        "singleQuote": false
      }
    },
    {
      "files": "*.sass",
      "options": {
        "parser": "scss"
      }
    }
  ]
}

Prettier does not reorder declarations or enforce property grouping — that is stylelint's domain. Prettier only handles whitespace, quotes, and line wrapping.

Stylelint: Stylistic vs Logic Rules

Stylelint fills the gap Prettier leaves. It catches logic errors (unknown properties, duplicate selectors, invalid color values) and can enforce stylistic rules (property order, selector patterns, unit preferences). The critical distinction:

  • Stylistic rules — spacing, quotes, color notation. These overlap with Prettier. If you run both tools, conflicts produce formatting fights on every save.
  • Logic rules — invalid declarations, duplicate properties, specificity issues. Prettier never touches these. Always keep them in stylelint.

The solution is stylelint-config-prettier (now merged into stylelint itself as of v15). Stylelint v15+ disables all stylistic rules by default. If you use an older config that re-enables them, extend stylelint-config-prettier last in your config array to turn them off again:

// .stylelintrc.json
{
  "extends": [
    "stylelint-config-standard-scss",
    "stylelint-config-prettier"
  ],
  "rules": {
    "scss/no-global-function-names": true,
    "scss/at-import-no-partial-leading-underscore": true,
    "no-duplicate-selectors": true,
    "no-descending-specificity": true
  }
}

Nesting: The 3-Level Max Rule

SCSS nesting generates compound selectors. Three levels of nesting produce a selector with three segments — .card .header .title — which is already specific enough to cause override headaches. Beyond three levels, generated CSS becomes brittle and hard to override without escalating specificity further.

The practical rule: nest at most 3 levels deep. Pseudo-classes and pseudo-elements (&:hover, &::before) do not count toward the limit since they don't add a selector segment.

// Bad — 4 levels deep
.nav {
  .list {
    .item {
      .link {         // 4th level: generates .nav .list .item .link
        color: blue;
      }
    }
  }
}

// Good — flat with BEM
.nav__link {
  color: blue;

  &:hover {           // pseudo-class: not a nesting level
    color: darkblue;
  }

  &--active {         // BEM modifier: 1 level
    font-weight: 600;
  }
}

Enforce the limit automatically with stylelint:

"max-nesting-depth": [3, { "ignore": ["blockless-at-rules", "pseudo-classes"] }]

@use and @forward Replacing @import

Sass deprecated @import and it will be removed in a future release. The replacements are @use and @forward:

  • @use — loads a module into a namespace. Variables, mixins, and functions are accessed as namespace.variable, preventing global namespace pollution.
  • @forward — re-exports a module's members so callers of your file can access them without importing the source directly. Use in _index.scss barrel files.
// _tokens.scss
$color-primary: #0057ff;
$spacing-base: 8px;

// _index.scss (barrel)
@forward "tokens";
@forward "mixins";

// component.scss
@use "../design-system" as ds;

.button {
  background: ds.$color-primary;
  padding: ds.$spacing-base * 2;
}

Prettier preserves @use and @forward declarations exactly, only normalizing whitespace. Stylelint's scss/at-use-no-unnamespaced rule flags bare @use without an as alias to keep namespace references explicit.

Sorting Declarations

Property order is a team convention, not a Prettier concern. The three common approaches:

StrategyDescriptionTooling
AlphabeticalProperties sorted A-Z. Easiest to learn, no grouping decisions.stylelint-order
GroupedPositioning, box model, typography, visual, animation.stylelint-config-recess-order
ConcentricOutside-in: position, size, border, background, text.stylelint-config-concentric-order
// .stylelintrc.json — grouped ordering via recess
{
  "extends": [
    "stylelint-config-standard-scss",
    "stylelint-config-recess-order"
  ]
}

Pick one strategy and stick to it. Mixing strategies across files or components produces the worst outcome — no strategy at all.

BEM with SCSS

BEM (Block Element Modifier) pairs naturally with SCSS's & parent selector. Elements use &__element and modifiers use &--modifier:

// card.scss
.card {
  border-radius: 8px;
  padding: 16px;

  &__header {
    font-size: 1.25rem;
    font-weight: 600;
  }

  &__body {
    color: #444;
  }

  &--featured {
    border: 2px solid #0057ff;
  }

  &--featured &__header {
    color: #0057ff;
  }
}

The &--featured &__header pattern generates .card--featured .card__header, which is the correct BEM compound selector. Keep this to a single level; deeper modifier-element combinations signal that the block should be split.

Stylelint enforces BEM naming via selector-class-pattern:

"selector-class-pattern": ["^[a-z][a-z0-9-]*(__[a-z0-9-]+(--[a-z0-9-]+)?|--[a-z0-9-]+)?$", {
  "message": "Class name must follow BEM convention"
}]

CSS Custom Properties vs SCSS Variables

SCSS variables ($color-primary) are compile-time constructs — they vanish in the output CSS. CSS custom properties (--color-primary) are runtime values, readable and writable by JavaScript, and can change inside media queries or with :root overrides.

Use casePrefer
Design tokens shared with JS (theming, dark mode)CSS custom properties
Internal SCSS math and mixinsSCSS variables
Values that vary per component instanceCSS custom properties
Build-time feature flagsSCSS variables

A common hybrid: define design tokens as CSS custom properties on :root, then reference them in SCSS using var(). This gives you runtime flexibility without losing SCSS's compile-time calculations.

// _tokens.scss
:root {
  --color-primary: #0057ff;
  --spacing-base: 8px;
}

// component.scss
@use "../tokens";

.button {
  // Runtime: responds to JS or :root overrides
  background: var(--color-primary);

  // Compile-time: used in SCSS math only
  $internal-padding: 12px;
  padding: $internal-padding ($internal-padding * 2);
}

Configuration and Pre-commit Setup

A minimal but complete formatting stack for SCSS projects:

npm install --save-dev prettier stylelint stylelint-config-standard-scss \
  stylelint-config-prettier stylelint-config-recess-order lint-staged husky
// .prettierrc
{
  "singleQuote": false,
  "printWidth": 100,
  "tabWidth": 2
}
// .stylelintrc.json
{
  "extends": [
    "stylelint-config-standard-scss",
    "stylelint-config-recess-order",
    "stylelint-config-prettier"
  ],
  "rules": {
    "max-nesting-depth": [3, {
      "ignore": ["blockless-at-rules", "pseudo-classes"]
    }],
    "scss/at-import-no-partial-leading-underscore": true,
    "scss/no-global-function-names": true,
    "no-duplicate-selectors": true,
    "no-descending-specificity": true
  }
}
// package.json — lint-staged config
{
  "lint-staged": {
    "*.scss": [
      "prettier --write",
      "stylelint --fix"
    ]
  }
}
# Install husky pre-commit hook
npx husky init
echo "npx lint-staged" > .husky/pre-commit

With this setup, every git commit automatically formats and lints staged SCSS files. Prettier runs first to normalize whitespace, then stylelint runs with --fix to apply auto-fixable rule violations.

Common Pitfalls

  • Interpolation in strings: #{$variable} inside quoted strings is valid SCSS but confuses some formatters. Prettier handles it correctly; older stylelint versions sometimes flag interpolated property values. Pin to a recent version.
  • @import deprecation: Sass will remove @import in a future major version. Run the official migration tool (sass-migrator module) to convert a codebase to @use/@forward automatically.
  • Map syntax changes: Sass maps now support trailing commas and unquoted keys in newer syntax. If your formatter strips trailing commas from maps, check that your Prettier version supports the Sass map syntax update (Prettier 3.x+).
  • Global SCSS functions: Functions like lighten() and darken() are deprecated in favor of color.adjust() from the sass:color module. Stylelint's scss/no-global-function-names rule flags these at lint time.

Paste any SCSS file into the SCSS Formatter to instantly apply Prettier formatting in your browser — no installation required. Once you are satisfied with the output, add the toolchain above to enforce the same style on every commit.