Skip to content

Advanced SCSS Best Practices

When to Use

Use this for complex Bootstrap customizations requiring Dart Sass features, deep map merging, performance optimization, or accessibility patterns.

Dart Sass vs LibSass (CRITICAL)

Tool Status Action
LibSass (node-sass) DEPRECATED Never use
Dart Sass (sass) REQUIRED Always use
# ❌ WRONG
npm install node-sass

# ✅ CORRECT
npm install sass

Why Dart Sass is required: - LibSass officially deprecated (2020) - Missing modern CSS features (:is(), :where(), container queries) - Module system (@use, @forward) only works in Dart Sass - Future Bootstrap versions require Dart Sass - Only Dart Sass receives security updates

Map Manipulation Gotchas

Deep Merge Problem

// ❌ WRONG: map-merge() doesn't deep merge
$base: ( button: ( padding: 1rem, color: blue ) );
$custom: ( button: ( color: red ) );
$merged: map-merge($base, $custom);
// Result: button.padding LOST

// ✅ CORRECT: Use map.deep-merge() (Dart Sass 1.27+)
@use "sass:map";
$merged: map.deep-merge($base, $custom);
// Result: button.padding preserved, color overridden

Bootstrap implication: When extending Bootstrap maps with nested structures, ALWAYS use map.deep-merge().

Import Order (CRITICAL)

Step Import Why
1 Functions Required for color operations
2 Custom variables Overrides before Bootstrap reads them
3 Bootstrap variables Reads your overrides via !default
4 Custom map extensions After Bootstrap maps loaded
5 Bootstrap core Maps, mixins, root
6 Bootstrap components Full framework
7 Custom utilities Before API generation
8 Utilities API Generates classes from maps
9 Custom components Last
// ✅ CORRECT ORDER
@import "bootstrap/scss/functions";
$primary: #0066cc;                                    // Override
@import "bootstrap/scss/variables";                   // Reads override
$theme-colors: map-merge($theme-colors, ("brand": ...)); // Extend
@import "bootstrap/scss/maps";
@import "bootstrap/scss/bootstrap";
$utilities: map-merge($utilities, (...));
@import "bootstrap/scss/utilities/api";
@import "custom-components";                          // Last

CSS Custom Properties vs SCSS Variables

Use Case Tool Why
Brand colors (never change) SCSS Compile-time calculation
Media queries SCSS CSS vars don't work in @media
Bootstrap overrides SCSS Bootstrap uses SCSS vars
Light/dark mode CSS vars Runtime theming
Component customization CSS vars Local overrides
JS-driven updates CSS vars Dynamic values

Best: Hybrid approach

// SCSS variables as source
$primary: #0066cc;

// Generate CSS custom properties
:root {
  --primary: #{$primary};
  --primary-rgb: #{red($primary), green($primary), blue($primary)};
}

// Use CSS vars in components (runtime)
.component {
  background: var(--primary);
  color: rgba(var(--primary-rgb), 0.5);
}

// Use SCSS for compile-time operations
.component-hover {
  background: darken($primary, 10%); // Can't use CSS vars
}

Performance Best Practices

Bundle Size Optimization

// ❌ WRONG: Import entire Bootstrap (200KB+)
@import "bootstrap/scss/bootstrap";

// ✅ CORRECT: Import only what you need (~50KB)
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/forms";

JavaScript Tree-Shaking

// ❌ WRONG
import 'bootstrap';

// ✅ CORRECT
import { Modal, Dropdown } from 'bootstrap';

PurgeCSS

// purgecss.config.js
module.exports = {
  content: ['./src/**/*.html', './src/**/*.js'],
  css: ['./dist/css/bootstrap.css'],
  safelist: ['show', 'collapse', 'collapsing', /^modal/, /^dropdown/]
};

Real-world impact: PurgeCSS reduces Bootstrap from 227KB to ~6KB.

Accessibility Best Practices

Color Contrast

// ✅ CORRECT: Use Bootstrap's contrast functions
$bg: #0066cc;
$text: color-contrast($bg); // Auto-selects white/black for 4.5:1

// WCAG 2.2 requirements:
// - Normal text: 4.5:1 minimum
// - Large text: 3:1 minimum
// - UI components: 3:1 minimum

Tool: WebAIM Contrast Checker

Focus Indicators (CRITICAL)

// ❌ WRONG: Never remove focus
button:focus {
  outline: none; // NEVER DO THIS
}

// ✅ CORRECT: Customize focus ring
$focus-ring-width: 0.25rem;
$focus-ring-color: rgba($primary, 0.25);

// OR: Keyboard-only focus
button:focus-visible {
  outline: 2px solid $primary;
  outline-offset: 2px;
}

button:focus:not(:focus-visible) {
  outline: none; // Remove for mouse only
}

WHY: Focus rings are essential for keyboard navigation.

Reduced Motion

// Bootstrap respects prefers-reduced-motion automatically

// ✅ Your custom animations should too
@media (prefers-reduced-motion: reduce) {
  .custom-animation {
    animation: none;
    transition: none;
  }
}

// Bootstrap variable
$enable-transitions: true; // Set false to disable globally

Screen Reader Utilities

// ✅ CORRECT: .visually-hidden (accessible)
.visually-hidden {
  // Invisible visually, accessible to screen readers
  // Bootstrap provides this
}

// ❌ WRONG: display:none (inaccessible)
.hidden {
  display: none; // NOT accessible
}

Common Mistakes

  • Wrong: Using LibSass/node-sass → Right: Use Dart Sass (sass package)
  • Wrong: map-merge() for nested maps → Right: map.deep-merge()
  • Wrong: Importing custom vars after Bootstrap → Right: Custom vars before Bootstrap variables
  • Wrong: outline: none on focus → Right: Customize focus-visible

See Also