Mixins, Functions, and Operations in Sass/SCSS

Module 7: CSS Preprocessors & Frameworks

Understanding Mixins

Mixins are one of the most powerful features in Sass. They allow you to define reusable blocks of CSS declarations that can be included (or "mixed in") within other selectors. Think of them as functions that output CSS code.

Analogy: Mixins as Cookie Cutters

Mixins are like cookie cutters for your CSS. Instead of manually creating the same pattern over and over (and risking inconsistencies), you create a cookie cutter (mixin) that guarantees the same output every time you use it. You can have different cookie cutters for different purposes, and you can even customize the "dough" (parameters) each time you use them.

Basic Mixin Syntax

// Defining a mixin
@mixin center-block {
  display: block;
  margin-left: auto;
  margin-right: auto;
}

// Using the mixin
.container {
  @include center-block;
  width: 80%;
  max-width: 1200px;
}

.profile-image {
  @include center-block;
  width: 150px;
  border-radius: 50%;
}

Mixins with Parameters

Mixins become even more powerful when they accept parameters, allowing for customization each time they're used:

// Mixin with parameters
@mixin box-shadow($x, $y, $blur, $spread, $color) {
  -webkit-box-shadow: $x $y $blur $spread $color;
  -moz-box-shadow: $x $y $blur $spread $color;
  box-shadow: $x $y $blur $spread $color;
}

// Using the mixin with different values
.card {
  @include box-shadow(0, 2px, 5px, 0, rgba(0, 0, 0, 0.1));
}

.dropdown {
  @include box-shadow(0, 5px, 10px, 2px, rgba(0, 0, 0, 0.2));
}

Default Parameter Values

You can provide default values for parameters, making them optional:

@mixin box-shadow($x: 0, $y: 2px, $blur: 4px, $spread: 0, $color: rgba(0, 0, 0, 0.1)) {
  -webkit-box-shadow: $x $y $blur $spread $color;
  -moz-box-shadow: $x $y $blur $spread $color;
  box-shadow: $x $y $blur $spread $color;
}

// Using only some parameters
.card {
  @include box-shadow($blur: 10px, $color: rgba(0, 0, 0, 0.2));
  // Equivalent to: @include box-shadow(0, 2px, 10px, 0, rgba(0, 0, 0, 0.2));
}

Variable Arguments

For mixins that need to accept a variable number of arguments, use the ... notation:

// Mixin with variable arguments
@mixin transition($properties...) {
  -webkit-transition: $properties;
  -moz-transition: $properties;
  -o-transition: $properties;
  transition: $properties;
}

.button {
  @include transition(background-color 0.3s ease, color 0.2s linear);
}

.fade {
  @include transition(opacity 0.5s ease-out);
}

Content Blocks in Mixins

Mixins can also accept content blocks using @content, which is incredibly useful for things like media queries:

// Media query mixin
@mixin respond-to($breakpoint) {
  @if $breakpoint == 'small' {
    @media (max-width: 576px) {
      @content;
    }
  } @else if $breakpoint == 'medium' {
    @media (max-width: 768px) {
      @content;
    }
  } @else if $breakpoint == 'large' {
    @media (max-width: 992px) {
      @content;
    }
  } @else if $breakpoint == 'xlarge' {
    @media (max-width: 1200px) {
      @content;
    }
  }
}

// Usage
.container {
  max-width: 1200px;
  
  @include respond-to('large') {
    max-width: 900px;
  }
  
  @include respond-to('medium') {
    max-width: 700px;
  }
  
  @include respond-to('small') {
    max-width: 100%;
    padding: 0 15px;
  }
}

Real-World Example: Component-Based Design System

Here's how mixins might be used in a design system for a company like Shopify:

// _buttons.scss

// Base button styles
@mixin button-base {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: all 0.2s ease;
  
  &:focus {
    outline: none;
    box-shadow: 0 0 0 2px rgba(#5c6ac4, 0.5);
  }
}

// Button variants
@mixin button-primary {
  @include button-base;
  background-color: #5c6ac4;
  color: white;
  border: 1px solid #5c6ac4;
  
  &:hover {
    background-color: darken(#5c6ac4, 10%);
    border-color: darken(#5c6ac4, 10%);
  }
  
  &:active {
    background-color: darken(#5c6ac4, 15%);
    border-color: darken(#5c6ac4, 15%);
  }
  
  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

@mixin button-secondary {
  @include button-base;
  background-color: white;
  color: #5c6ac4;
  border: 1px solid #c4cdd5;
  
  &:hover {
    background-color: #f9fafb;
    border-color: #5c6ac4;
  }
  
  &:active {
    background-color: #f4f6f8;
    border-color: darken(#5c6ac4, 10%);
  }
  
  &:disabled {
    color: #919eab;
    border-color: #c4cdd5;
    cursor: not-allowed;
  }
}

@mixin button-critical {
  @include button-base;
  background-color: #de3618;
  color: white;
  border: 1px solid #de3618;
  
  &:hover {
    background-color: darken(#de3618, 10%);
    border-color: darken(#de3618, 10%);
  }
  
  &:active {
    background-color: darken(#de3618, 15%);
    border-color: darken(#de3618, 15%);
  }
  
  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

// Button sizes
@mixin button-size($size) {
  @if $size == 'small' {
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
  } @else if $size == 'medium' {
    padding: 0.75rem 1.5rem;
    font-size: 1rem;
  } @else if $size == 'large' {
    padding: 1rem 2rem;
    font-size: 1.125rem;
  }
}

// Usage in component file
.btn {
  &--primary {
    @include button-primary;
  }
  
  &--secondary {
    @include button-secondary;
  }
  
  &--critical {
    @include button-critical;
  }
  
  &--small {
    @include button-size('small');
  }
  
  &--medium {
    @include button-size('medium');
  }
  
  &--large {
    @include button-size('large');
  }
}

Functions in Sass

While mixins output CSS, functions in Sass return values that can be used in CSS properties or other Sass constructs. They allow you to perform calculations and manipulate values before they're used in your style rules.

Built-in Functions

Sass comes with many useful built-in functions for manipulating colors, numbers, lists, and more:

Color Functions

$base-color: #3498db;

.dark-button {
  background-color: darken($base-color, 15%);  // Makes color darker
  border-color: darken($base-color, 25%);
}

.light-button {
  background-color: lighten($base-color, 15%);  // Makes color lighter
  color: darken($base-color, 30%);
}

.highlight {
  background-color: saturate($base-color, 20%);  // Makes color more vibrant
}

.muted {
  color: desaturate($base-color, 30%);  // Makes color less vibrant
}

.overlay {
  background-color: rgba($base-color, 0.7);  // Adds transparency
}

.complement {
  border-color: complement($base-color);  // Gets complementary color
}

Math Functions

$base-size: 16px;

.circle {
  width: round($base-size * 3.14);  // Rounds to nearest whole number
  height: round($base-size * 3.14);
  border-radius: 50%;
}

.container {
  width: min(1200px, 100%);  // Takes the smaller of two values
  margin: 0 auto;
}

.column {
  width: percentage(1/3);  // Converts decimal to percentage (33.33333%)
}

List Functions

$sizes: 1rem, 1.5rem, 2rem, 2.5rem, 3rem;
$colors: #3498db, #2ecc71, #e74c3c, #f1c40f, #9b59b6;

.first {
  margin: nth($sizes, 1);  // Gets first item (1rem)
  color: nth($colors, 1);  // Gets first color (#3498db)
}

.list-length {
  --colors-count: #{length($colors)};  // Gets list length (5)
}

String Functions

$asset-path: 'assets/images/';

.logo {
  background-image: url('#{$asset-path}logo.png');  // String interpolation
  content: to-upper-case('logo');  // Converts string to uppercase ('LOGO')
}

Custom Functions

You can also define your own functions using @function:

// Function to convert pixels to rem
@function px-to-rem($px, $base-font-size: 16px) {
  @return ($px / $base-font-size) * 1rem;
}

// Function to get color from a palette map
@function color($palette, $shade: 'base') {
  $color-palette: (
    'primary': (
      'light': #5ab9ea,
      'base': #1a85d8,
      'dark': #0c5c99
    ),
    'secondary': (
      'light': #84dba7,
      'base': #4cc37b,
      'dark': #2aa158
    ),
    'neutral': (
      'light': #f5f5f5,
      'base': #e0e0e0,
      'dark': #9e9e9e
    )
  );
  
  @return map-get(map-get($color-palette, $palette), $shade);
}

// Using our custom functions
body {
  font-size: px-to-rem(16px);  // Outputs: 1rem
  color: color('neutral', 'dark');  // Outputs: #9e9e9e
}

h1 {
  font-size: px-to-rem(32px);  // Outputs: 2rem
  color: color('primary');  // Outputs: #1a85d8 (uses default 'base' shade)
}

.button {
  background-color: color('secondary');  // Outputs: #4cc37b
  padding: px-to-rem(12px) px-to-rem(24px);  // Outputs: 0.75rem 1.5rem
  
  &:hover {
    background-color: color('secondary', 'dark');  // Outputs: #2aa158
  }
}

Functions vs. Mixins: When to Use Each

graph TB A{Need to Output CSS?} A -->|Yes| B[Use a Mixin] A -->|No| C[Use a Function] D{Need to Return a Value?} D -->|Yes| C D -->|No| B E{Want to Reuse Code?} E -->|"Yes (CSS Declarations)"| B E -->|"Yes (Calculations & Values)"| C
  • Use functions when you need to compute and return a value
  • Use mixins when you need to generate CSS code

Operations in Sass

Sass supports various operations on values, allowing you to calculate property values dynamically. This capability is particularly useful for creating consistent spacing, responsive layouts, and mathematical relationships in your design system.

Math Operations

$base-spacing: 8px;

.container {
  // Basic operations
  padding: $base-spacing * 2;  // 16px
  margin-bottom: $base-spacing * 3;  // 24px
  
  // Multiple operations
  width: 100% - 40px;  // Subtract pixels from percentage
  height: calc(100vh - #{$base-spacing * 8});  // Interpolate variable in calc()
  
  // Division (using math.div in Dart Sass)
  @use "sass:math";
  line-height: math.div(24px, 16px);  // 1.5
}

Division in Modern Sass

In modern Sass (Dart Sass 1.33.0+), the division operator (/) is deprecated for calculations. Instead, use the math.div() function from the built-in math module:

@use "sass:math";

// Old way (deprecated)
$result: 24px / 16px;  // This will eventually stop working

// New way
$result: math.div(24px, 16px);  // This is the preferred approach

Color Operations

$brand-blue: #1a85d8;
$brand-yellow: #f1c40f;

.gradient {
  // Mixing colors
  background: linear-gradient($brand-blue, $brand-yellow);
  
  // Color math (generally better to use color functions instead)
  border-color: $brand-blue + #111111;  // Adds RGB values
  color: $brand-blue - #111111;  // Subtracts RGB values
}

String Operations

$asset-path: 'assets/';
$image-folder: 'images/';
$file-name: 'header-bg.jpg';

.header {
  // String concatenation with +
  background-image: url($asset-path + $image-folder + $file-name);
  
  // String interpolation (preferred method)
  background-image: url('#{$asset-path}#{$image-folder}#{$file-name}');
}

Creating a Spacing System with Operations

One practical application of operations is to create a consistent spacing system:

// Define a base unit
$space-unit: 8px;

// Create a spacing scale using multiples of the base unit
$space-xs: $space-unit * 0.5;    // 4px
$space-sm: $space-unit;          // 8px
$space-md: $space-unit * 2;      // 16px
$space-lg: $space-unit * 3;      // 24px
$space-xl: $space-unit * 4;      // 32px
$space-xxl: $space-unit * 6;     // 48px
$space-xxxl: $space-unit * 8;    // 64px

// Create spacing utility classes
.m {
  &-0 { margin: 0; }
  &-xs { margin: $space-xs; }
  &-sm { margin: $space-sm; }
  &-md { margin: $space-md; }
  &-lg { margin: $space-lg; }
  &-xl { margin: $space-xl; }
  
  // Directional variants
  &t {
    &-0 { margin-top: 0; }
    &-xs { margin-top: $space-xs; }
    &-sm { margin-top: $space-sm; }
    &-md { margin-top: $space-md; }
    &-lg { margin-top: $space-lg; }
    &-xl { margin-top: $space-xl; }
  }
  
  // Additional directions (right, bottom, left) would follow the same pattern
}

// Same pattern for padding classes...

Real-World Example: Creating a Responsive Type Scale

Many design systems, like Material Design or Apple's Human Interface Guidelines, use a type scale based on mathematical relationships. Here's how we might implement a responsive type scale using Sass operations and functions:

@use "sass:math";

// Base font size
$base-font-size: 16px;

// Scale factor (1.25 = major third)
$scale-ratio: 1.25;

// Function to calculate type size based on scale
@function type-scale($level) {
  @return math.pow($scale-ratio, $level) * $base-font-size;
}

// Convert pixel values to rem
@function rem($pixels) {
  @return math.div($pixels, $base-font-size) * 1rem;
}

// Generate our type scale
$type-xs: type-scale(-2);    // ~10.2px
$type-sm: type-scale(-1);    // ~12.8px
$type-base: type-scale(0);   // 16px
$type-md: type-scale(1);     // ~20px
$type-lg: type-scale(2);     // ~25px
$type-xl: type-scale(3);     // ~31.3px
$type-xxl: type-scale(4);    // ~39.1px
$type-xxxl: type-scale(5);   // ~48.8px

// Typography styles
body {
  font-size: rem($base-font-size);
  line-height: 1.5;
}

h1 {
  font-size: rem($type-xxxl);
  line-height: 1.1;
  
  @media (max-width: 768px) {
    // Scale down on smaller screens
    font-size: rem($type-xxl);
  }
}

h2 {
  font-size: rem($type-xxl);
  line-height: 1.2;
  
  @media (max-width: 768px) {
    font-size: rem($type-xl);
  }
}

h3 {
  font-size: rem($type-xl);
  line-height: 1.3;
}

// And so on for other elements...

Control Directives

Sass provides control directives that let you apply programming logic in your stylesheets, making your code more flexible and dynamic.

@if / @else if / @else

// Mixin for text contrast based on background
@mixin text-contrast($background) {
  @if (lightness($background) > 60%) {
    color: #333333; // Dark text for light backgrounds
  } @else {
    color: #ffffff; // Light text for dark backgrounds
  }
}

.alert {
  &--success {
    background-color: #28a745;
    @include text-contrast(#28a745);
  }
  
  &--warning {
    background-color: #ffc107;
    @include text-contrast(#ffc107);
  }
  
  &--danger {
    background-color: #dc3545;
    @include text-contrast(#dc3545);
  }
}

@for Loops

// Generate grid classes
@for $i from 1 through 12 {
  .col-#{$i} {
    width: ($i / 12) * 100%;
  }
}

// Generate margin utilities with increasing sizes
@for $i from 1 through 5 {
  .m-#{$i} {
    margin: $i * 0.5rem;
  }
}

@each Loops

// Generate utility classes for colors
$colors: (
  'primary': #3498db,
  'success': #2ecc71,
  'warning': #f1c40f,
  'danger': #e74c3c,
  'dark': #34495e
);

@each $name, $color in $colors {
  .text-#{$name} {
    color: $color;
  }
  
  .bg-#{$name} {
    background-color: $color;
  }
  
  .border-#{$name} {
    border-color: $color;
  }
}

// Generate classes for a list of items
$sizes: 'small', 'medium', 'large';

@each $size in $sizes {
  .btn--#{$size} {
    @if $size == 'small' {
      padding: 0.5rem 1rem;
      font-size: 0.875rem;
    } @else if $size == 'medium' {
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
    } @else if $size == 'large' {
      padding: 1rem 2rem;
      font-size: 1.125rem;
    }
  }
}

@while Loops

// Generate increasingly transparent versions of a color
$color: #3498db;
$opacity: 1;
$i: 10;

@while $i > 0 {
  .bg-primary-#{$i * 10} {
    background-color: rgba($color, $opacity);
  }
  
  $opacity: $opacity - 0.1;
  $i: $i - 1;
}

Real-World Example: Building a Complete Utility Library

Here's how you might generate a set of utility classes similar to Tailwind CSS, using control directives:

// _utilities.scss

// Color palette
$colors: (
  'primary': #3490dc,
  'secondary': #ffed4a,
  'danger': #e3342f,
  'success': #38c172,
  'warning': #f6993f,
  'dark': #2d3748,
  'gray': (
    '100': #f7fafc,
    '200': #edf2f7,
    '300': #e2e8f0,
    '400': #cbd5e0,
    '500': #a0aec0,
    '600': #718096,
    '700': #4a5568,
    '800': #2d3748,
    '900': #1a202c
  )
);

// Spacing scale
$spacing-unit: 0.25rem;
$spacing-scale: (
  '0': 0,
  '1': 1,
  '2': 2,
  '3': 3,
  '4': 4,
  '5': 5,
  '6': 6,
  '8': 8,
  '10': 10,
  '12': 12,
  '16': 16,
  '20': 20,
  '24': 24,
  '32': 32,
  '40': 40,
  '48': 48,
  '64': 64
);

// Font sizes
$font-sizes: (
  'xs': 0.75rem,
  'sm': 0.875rem,
  'base': 1rem,
  'lg': 1.125rem,
  'xl': 1.25rem,
  '2xl': 1.5rem,
  '3xl': 1.875rem,
  '4xl': 2.25rem,
  '5xl': 3rem,
  '6xl': 4rem
);

// Generate spacing utilities
@each $space-name, $space-multiplier in $spacing-scale {
  $value: $spacing-unit * $space-multiplier;
  
  // Margin utilities
  .m-#{$space-name} { margin: $value; }
  .mt-#{$space-name} { margin-top: $value; }
  .mr-#{$space-name} { margin-right: $value; }
  .mb-#{$space-name} { margin-bottom: $value; }
  .ml-#{$space-name} { margin-left: $value; }
  .mx-#{$space-name} {
    margin-left: $value;
    margin-right: $value;
  }
  .my-#{$space-name} {
    margin-top: $value;
    margin-bottom: $value;
  }
  
  // Padding utilities (following the same pattern)
  .p-#{$space-name} { padding: $value; }
  .pt-#{$space-name} { padding-top: $value; }
  // ... and so on
}

// Generate text color utilities
@each $color-name, $color-value in $colors {
  @if type-of($color-value) == 'map' {
    @each $shade-name, $shade-value in $color-value {
      .text-#{$color-name}-#{$shade-name} {
        color: $shade-value;
      }
    }
  } @else {
    .text-#{$color-name} {
      color: $color-value;
    }
  }
}

// Generate background color utilities
@each $color-name, $color-value in $colors {
  @if type-of($color-value) == 'map' {
    @each $shade-name, $shade-value in $color-value {
      .bg-#{$color-name}-#{$shade-name} {
        background-color: $shade-value;
      }
    }
  } @else {
    .bg-#{$color-name} {
      background-color: $color-value;
    }
  }
}

// Generate font size utilities
@each $size-name, $size-value in $font-sizes {
  .text-#{$size-name} {
    font-size: $size-value;
  }
}

// Generate width utilities (percentages and fixed)
@for $i from 1 through 6 {
  .w-#{$i*10} {
    width: $i * 10%;
  }
}

.w-full { width: 100%; }
.w-auto { width: auto; }
.w-screen { width: 100vw; }

// Similar patterns could be used for borders, flexbox, grid, etc.

Putting It All Together

Let's see how mixins, functions, and operations can be combined to create a flexible, maintainable styling system:

1. Define Variables and Functions

// _variables.scss
$colors: (
  'primary': #4e73df,
  'success': #1cc88a,
  'info': #36b9cc,
  'warning': #f6c23e,
  'danger': #e74a3b
);

$font-family-sans: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
$font-family-base: $font-family-sans;

$spacer: 1rem;
$spacers: (
  0: 0,
  1: $spacer * 0.25,
  2: $spacer * 0.5,
  3: $spacer,
  4: $spacer * 1.5,
  5: $spacer * 3
);

// _functions.scss
@function color($key) {
  @return map-get($colors, $key);
}

@function spacer($key) {
  @return map-get($spacers, $key);
}

2. Create Mixins

// _mixins.scss
@mixin box-shadow($shadow...) {
  -webkit-box-shadow: $shadow;
  box-shadow: $shadow;
}

@mixin transition($transition...) {
  -webkit-transition: $transition;
  transition: $transition;
}

@mixin border-radius($radius: 0.35rem) {
  border-radius: $radius;
}

@mixin flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

@mixin responsive($breakpoint) {
  @if $breakpoint == 'sm' {
    @media (min-width: 576px) { @content; }
  } @else if $breakpoint == 'md' {
    @media (min-width: 768px) { @content; }
  } @else if $breakpoint == 'lg' {
    @media (min-width: 992px) { @content; }
  } @else if $breakpoint == 'xl' {
    @media (min-width: 1200px) { @content; }
  }
}

3. Build Components

// _buttons.scss
@import 'variables';
@import 'functions';
@import 'mixins';

.btn {
  display: inline-block;
  font-weight: 400;
  text-align: center;
  white-space: nowrap;
  vertical-align: middle;
  user-select: none;
  border: 1px solid transparent;
  padding: spacer(2) spacer(3);
  font-size: 1rem;
  line-height: 1.5;
  @include border-radius();
  @include transition(color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out);
  
  &:focus, &:hover {
    text-decoration: none;
    outline: 0;
  }
  
  // Generate button variants using color map
  @each $name, $value in $colors {
    &-#{$name} {
      color: white;
      background-color: $value;
      border-color: $value;
      
      &:hover {
        background-color: darken($value, 10%);
        border-color: darken($value, 12%);
      }
      
      &:focus {
        @include box-shadow(0 0 0 0.2rem rgba($value, 0.25));
      }
      
      &:disabled {
        background-color: rgba($value, 0.5);
        border-color: rgba($value, 0.5);
      }
    }
  }
  
  // Size variants
  &-sm {
    padding: spacer(1) spacer(2);
    font-size: 0.875rem;
    @include border-radius(0.2rem);
  }
  
  &-lg {
    padding: spacer(3) spacer(4);
    font-size: 1.25rem;
    @include border-radius(0.5rem);
  }
}

4. Use in a Page

// main.scss
@import 'variables';
@import 'functions';
@import 'mixins';
@import 'buttons';

.app-container {
  font-family: $font-family-base;
  padding: spacer(4);
  
  @include responsive('md') {
    padding: spacer(5);
  }
}

.action-panel {
  @include flex-center;
  margin-top: spacer(4);
  
  > * {
    margin: 0 spacer(2);
  }
}

Practice Activities

  1. Media Query Mixin: Create a comprehensive media query mixin that handles both min-width and max-width queries, as well as device-specific conditions (mobile, tablet, desktop).
  2. Typography System: Build a typography system with functions and mixins to control font sizes, weights, and line heights consistently across your site.
  3. Mini Design System: Create a small design system with variables, functions, and mixins for:
    • A color palette with at least 5 colors and multiple shades
    • A spacing system based on a base unit
    • Button components with variants (primary, secondary, danger)
    • Card components with different states
  4. Utility Generator: Write a Sass file that generates utility classes for margins, paddings, and colors using @each and @for loops.

Additional Resources

Key Takeaways

Next Up

In our next session, we'll explore advanced Sass concepts including inheritance and the @extend directive, control directives and loops, and Sass architecture and organization.