Skip to content

Sass Theming — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Sass Theming. We cover key concepts, practical examples, and best practices to help you master this topic.

Sass theming uses maps, color functions, and CSS custom properties to create maintainable theme systems with light and dark mode support.

What You'll Learn

  • Theme maps for design tokens
  • CSS custom properties from Sass variables
  • Light and dark theme generation
  • Color manipulation for theme variants
  • Runtime theme switching
  • Theme compilation strategies

Why It Matters

  • Theming is a common requirement for modern sites
  • Manual theme maintenance is error-prone
  • Sass automates theme generation from a single source
  • Runtime switching requires CSS custom properties

Real-World Use

  • A SaaS app offers light and dark mode
  • A design system ships with multiple brand themes
  • A user dashboard allows custom accent colors
  • A marketing site switches themes seasonally
flowchart LR
  A[Sass Theming] --> B[Theme Map]
  B --> C[Generate CSS Variables]
  B --> D[Generate Light Theme]
  B --> E[Generate Dark Theme]
  D --> F[:root { --var: value }]
  E --> G[.dark { --var: value }]
  F --> H[Runtime switching]
  G --> H

Theming Approaches

Code Example: Theme Map with CSS Custom Properties

// Theme tokens as Sass map
$light-theme: (
    'color-bg': #ffffff,
    'color-bg-secondary': #f8f9fa,
    'color-text': #333333,
    'color-text-secondary': #6c757d,
    'color-primary': #0066CC,
    'color-primary-hover': #0052a3,
    'color-primary-light': #e6f2ff,
    'color-border': #dee2e6,
    'color-success': #28a745,
    'color-danger': #dc3545,
    'color-warning': #ffc107,
    'shadow-sm': '0 1px 2px rgba(0,0,0,0.05)',
    'shadow-md': '0 4px 6px rgba(0,0,0,0.1)',
    'shadow-lg': '0 10px 15px rgba(0,0,0,0.1)'
);

$dark-theme: (
    'color-bg': #1a1a2e,
    'color-bg-secondary': #2d2d44,
    'color-text': #e0e0e0,
    'color-text-secondary': #a0a0b0,
    'color-primary': #4da6ff,
    'color-primary-hover': #80bfff,
    'color-primary-light': #1a3a5c,
    'color-border': #404060,
    'color-success': #48c774,
    'color-danger': #f06060,
    'color-warning': #ffd93d,
    'shadow-sm': '0 1px 2px rgba(0,0,0,0.3)',
    'shadow-md': '0 4px 6px rgba(0,0,0,0.4)',
    'shadow-lg': '0 10px 15px rgba(0,0,0,0.5)'
);

// Mixin to generate CSS custom properties from a theme map
@mixin generate-theme($theme, $prefix: '') {
    @each $name, $value in $theme {
        --#{$prefix}#{$name}: #{$value};
    }
}

// Generate themes
:root {
    @include generate-theme($light-theme);
}

[data-theme="dark"] {
    @include generate-theme($dark-theme);
}

// Use in components
.card {
    background: var(--color-bg);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    box-shadow: var(--shadow-md);

    &__title {
        color: var(--color-text);
    }

    &__text {
        color: var(--color-text-secondary);
    }

    .btn-primary {
        background: var(--color-primary);
        color: #fff;

        &:hover {
            background: var(--color-primary-hover);
        }
    }
}

Expected output: The :root block defines light theme CSS custom properties. [data-theme="dark"] overrides them for dark mode. Components use var() for all themed values. Switching the data-theme attribute on a parent element changes the entire page theme.

Code Example: Theme Functions and Lookups

@use 'sass:map';
@use 'sass:color';

// Theme value lookup
@function theme($key, $theme: $light-theme) {
    @if map.has-key($theme, $key) {
        @return map.get($theme, $key);
    } @else {
        @error "Theme key not found: #{$key}";
    }
}

// Color variant generation
@function theme-light-variant($key, $amount: 40%) {
    $color: theme($key);
    @return color.mix($color, white, (100% - $amount));
}

@function theme-dark-variant($key, $amount: 20%) {
    $color: theme($key);
    @return color.mix($color, black, (100% - $amount));
}

// Usage in mixin that outputs both static and dynamic values
@mixin themed-component {
    // Static fallbacks (for non-JS environments)
    background: theme('color-bg');
    color: theme('color-text');
    border-color: theme('color-border');

    // Dynamic values via CSS custom properties
    background: var(--color-bg);
    color: var(--color-text);
    border-color: var(--color-border);
}

// Generate component variants
@mixin button-variant-themed($name, $light-color, $dark-color) {
    .btn-#{$name} {
        background: $light-color;
        color: #fff;

        [data-theme="dark"] & {
            background: $dark-color;
        }

        // Use CSS custom properties for dynamic switching
        background: var(--btn-#{$name}-bg);
    }
}

// Pre-generate button variants
:root {
    --btn-primary-bg: #{theme('color-primary')};
    --btn-danger-bg: #{theme('color-danger')};
}

[data-theme="dark"] {
    --btn-primary-bg: #{theme('color-primary', $dark-theme)};
    --btn-danger-bg: #{theme('color-danger', $dark-theme)};
}

Expected output: The theme() function provides compile-time lookup. CSS custom properties provide runtime switching. Components can use both: static values for initial render and custom properties for dynamic updates.

Code Example: Advanced Theming with Color Functions

@use 'sass:color';
@use 'sass:map';

// Base brand colors (single source of truth)
$brand: (
    'primary': #0066CC,
    'secondary': #6c757d,
    'accent': #ff6600
);

// Generate themes from brand colors
@function generate-theme-from-brand($mode: 'light') {
    $primary: map.get($brand, 'primary');
    $secondary: map.get($brand, 'secondary');

    @if $mode == 'light' {
        @return (
            'color-bg': #ffffff,
            'color-bg-secondary': #f8f9fa,
            'color-text': #333333,
            'color-text-secondary': #6c757d,
            'color-primary': $primary,
            'color-primary-hover': color.adjust($primary, $lightness: -10%),
            'color-primary-light': color.adjust($primary, $lightness: 40%),
            'color-primary-dark': color.adjust($primary, $lightness: -20%),
            'color-secondary': $secondary,
            'color-accent': map.get($brand, 'accent'),
            'color-border': #dee2e6,
            'color-success': #28a745,
            'color-danger': #dc3545,
            'shadow-sm': '0 1px 2px rgba(0,0,0,0.05)',
            'shadow-md': '0 4px 6px rgba(0,0,0,0.1)',
            'shadow-lg': '0 10px 15px rgba(0,0,0,0.1)'
        );
    } @else {
        @return (
            'color-bg': #1a1a2e,
            'color-bg-secondary': #2d2d44,
            'color-text': #e0e0e0,
            'color-text-secondary': #a0a0b0,
            'color-primary': color.adjust($primary, $lightness: 30%),
            'color-primary-hover': color.adjust($primary, $lightness: 40%),
            'color-primary-light': color.adjust($primary, $lightness: -20%),
            'color-primary-dark': color.adjust($primary, $lightness: -40%),
            'color-secondary': color.adjust($secondary, $lightness: 30%),
            'color-accent': color.adjust(map.get($brand, 'accent'), $lightness: 20%),
            'color-border': #404060,
            'color-success': #48c774,
            'color-danger': #f06060,
            'shadow-sm': '0 1px 2px rgba(0,0,0,0.3)',
            'shadow-md': '0 4px 6px rgba(0,0,0,0.4)',
            'shadow-lg': '0 10px 15px rgba(0,0,0,0.5)'
        );
    }
}

// Generate themes dynamically
$light-theme: generate-theme-from-brand('light');
$dark-theme: generate-theme-from-brand('dark');

// Create alternate brand theme
$alt-brand: (
    'primary': #7c3aed,  // Purple
    'secondary': #6b7280,
    'accent': #f59e0b     // Amber
);

$alt-light-theme: generate-theme-from-brand('light');  // Uses $alt-brand context

Expected output: The generate-theme-from-brand() function automatically creates light and dark themes from just 3 brand colors. Color functions programmatically compute hover, light, and dark variants.

Code Example: CSS-Only Theme Switching

// Theme switching without JavaScript
// base.scss
:root {
    // Default light theme
    @include generate-theme($light-theme);

    // Auto dark theme via prefers-color-scheme
    @media (prefers-color-scheme: dark) {
        @include generate-theme($dark-theme);
    }
}

// Theme toggle allows manual override
// Handled by JavaScript: document.documentElement.setAttribute('data-theme', 'dark')
// Or by a class: document.body.classList.toggle('dark-mode')

// Multiple themes
@mixin theme-variant($name, $theme-map) {
    [data-theme="#{$name}"] {
        @include generate-theme($theme-map);
    }
}

// Generate multiple themes
:root { @include generate-theme($light-theme); }
@include theme-variant('dark', $dark-theme);
@include theme-variant('high-contrast', $high-contrast-theme);
@include theme-variant('sepia', $sepia-theme);

// Transition between themes
* {
    transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}

@media (prefers-reduced-motion: reduce) {
    * {
        transition: none;
    }
}

Expected output: The base theme is the light theme. prefers-color-scheme: dark automatically applies the dark theme for users with that OS setting. Multiple themes (dark, high-contrast, sepia) can be applied with a data attribute.

Common Mistakes

  1. Using only static Sass variables for theming — Static variables cannot change at runtime. Use CSS custom properties for dynamic theme switching.
  2. Not providing fallback values — Some browsers do not support CSS custom properties. Provide static fallback values before var().
  3. Overwriting all properties in dark theme — Only override properties that actually change. Let the light theme cascade through.
  4. Not using prefers-color-scheme — Users set their OS preference. Respect it with the media query, not just a toggle button.
  5. Generating themes inside components — Generate themes once at :root level. Components should consume, not define, theme values.
  6. Missing contrast in dark mode — Dark theme colors should maintain WCAG contrast ratios. Test text-on-background contrast for both themes.
  7. Not testing theme switching — Switching between themes should be smooth. Test for flashing or jarring transitions.

Practice Questions

  1. Why combine Sass variables with CSS custom properties for theming? Sass variables provide compile-time generation. CSS custom properties provide runtime theme switching without reloading CSS.
  2. How do you detect dark mode preference in CSS? @media (prefers-color-scheme: dark) { ... }
  3. What is the advantage of generating themes from brand colors? Changing 3 brand colors regenerates the entire theme palette (light, dark, hover, border, background variants).
  4. How do you provide fallback values for CSS custom properties? Provide a static value before var(): color: #333; color: var(--color-text);.

FAQ

Should I use Sass or CSS for theming?

Use both. Use Sass to generate theme maps and CSS custom properties for runtime values. This combines compile-time automation with runtime flexibility.

How do I handle theme-specific images?

Use separate image paths in your theme map: --logo-url: url('/images/logo-light.svg'). Or use CSS filter: invert(1) for simple cases.

What about performance of CSS custom properties?

CSS custom properties are very performant. They do not trigger repaints when values change. Theme switching is effectively free.

Mini Project

Build a complete theming system with Sass. Create: (1) a $brand map with 3 base colors, (2) a generate-theme() function that creates both light and dark theme maps from the brand colors, (3) a generate-theme mixin that outputs CSS custom properties, (4) :root light theme, (5) @media (prefers-color-scheme: dark) for OS-level dark mode, (6) a [data-theme="dark"] selector for manual toggle, (7) a "high-contrast" theme variant, (8) 5 components (button, card, nav, alert, form) that use var() for all themed values, and (9) smooth transitions between themes. Test theme switching via DevTools and verify all components update.

What's Next

Continue with Lesson 19: Less Setup and Basics to learn the Less preprocessor.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro