Skip to main content

⚙️ Mixins, Functions, and Operations

This is where Sass stops being "CSS with variables" and starts being a real styling engine. Mixins package reusable declarations, functions compute and return values, operations do math on your design tokens, and control directives generate whole families of CSS. Together they power every serious design system.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Write mixins with parameters, default values, variable arguments, and @content blocks
  • Create custom functions and distinguish clearly when to use a function vs a mixin
  • Use the sass:math and sass:color modules for modern, non-deprecated operations
  • Apply control directives@if, @for, @each, @while — to generate CSS
  • Combine all of these into a small, coherent design system

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Build a responsive-breakpoint mixin and a color-utility generator with @each.

In This Lesson

Understanding Mixins

A mixin is a named, reusable block of CSS declarations. You define it once with @mixin and drop it into any selector with @include. Think of it as a function whose output is CSS.

💡 Analogy — mixins are cookie cutters. Instead of hand-shaping the same pattern over and over (and risking wobbles), you press a cookie cutter to get an identical shape every time. Different cutters serve different purposes, and by passing "dough" (parameters) you can vary the result without redoing the cutter.
// Define once
@mixin center-block {
  display: block;
  margin-left: auto;
  margin-right: auto;
}

// Reuse anywhere
.container {
  @include center-block;
  width: 80%;
  max-width: 1200px;
}

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

Parameters, Defaults & @content

Parameters

Mixins become powerful when they accept arguments, letting each use customize the output:

@mixin box-shadow($x, $y, $blur, $spread, $color) {
  box-shadow: $x $y $blur $spread $color;
}

.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 values

Give parameters defaults to make them optional, then override only the ones you care about by name:

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

.card {
  // keyword arguments — skip the rest
  @include box-shadow($blur: 10px, $color: rgba(0, 0, 0, 0.2));
}

Variable arguments

Use ... to accept any number of arguments — ideal for shorthand properties:

@mixin 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 — passing a block in

A mixin can accept a whole block of rules via @content. This is the classic pattern for responsive breakpoints:

@use 'sass:map';

$breakpoints: ('small': 576px, 'medium': 768px, 'large': 992px);

@mixin respond-to($name) {
  $width: map.get($breakpoints, $name);
  @media (max-width: $width) {
    @content;   // whatever you pass drops in here
  }
}

.container {
  max-width: 1200px;

  @include respond-to('medium') {
    max-width: 700px;
    padding: 0 15px;
  }
}

📖 Real-world: a button system

Design systems (think Shopify's Polaris) lean heavily on mixins to keep buttons consistent. A shared button-base mixin is composed into each variant:

@use 'sass:color';
$brand: #5c6ac4;

@mixin button-base {
  display: inline-flex;
  align-items: center;
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
}

@mixin button-primary {
  @include button-base;
  background-color: $brand;
  color: white;

  &:hover {
    // modern replacement for the deprecated darken()
    background-color: color.adjust($brand, $lightness: -10%);
  }
}

.btn--primary { @include button-primary; }

Functions in Sass

Where a mixin outputs CSS, a function returns a value you can drop into any property. Sass ships a rich set of built-in functions, now organized into modules you load with @use.

Built-in module functions

@use 'sass:color';
@use 'sass:math';
@use 'sass:list';

$base: #3498db;

.button {
  // color module (replaces old global darken/lighten/saturate)
  background-color: color.adjust($base, $lightness: -15%);
  border-color: color.scale($base, $lightness: -25%);
}

.overlay {
  background-color: rgba($base, 0.7);  // still works globally
}

.column {
  // percentage of a division
  width: math.percentage(math.div(1, 3));  // 33.333%
}

$sizes: 1rem, 1.5rem, 2rem;
.first { margin: list.nth($sizes, 1); } // 1rem

⚠️ Deprecated: global darken() / lighten()

The old global color functions like darken($c, 10%) and lighten() are deprecated in current Dart Sass. Use the sass:color module instead: color.adjust($c, $lightness: -10%) for a fixed shift, or color.scale($c, $lightness: -10%) for a proportional one (usually the nicer result).

Custom functions

Define your own with @function and @return. A pixels-to-rem converter and a palette lookup are classic examples:

@use 'sass:math';
@use 'sass:map';

@function px-to-rem($px, $base: 16px) {
  @return math.div($px, $base) * 1rem;
}

$palette: (
  'primary':   ('base': #1a85d8, 'dark': #0c5c99),
  'secondary': ('base': #4cc37b, 'dark': #2aa158)
);

@function color-token($name, $shade: 'base') {
  @return map.get(map.get($palette, $name), $shade);
}

h1 {
  font-size: px-to-rem(32px);            // 2rem
  color: color-token('primary');         // #1a85d8
}
.button:hover {
  background: color-token('secondary', 'dark'); // #2aa158
}

Function vs Mixin

The distinction is simple once you internalize it: a mixin produces CSS declarations; a function produces a single value. If you find yourself wanting to @return something, you need a function. If you want to emit property: value pairs, you need a mixin.

graph TB A{What do you need out?} -->|CSS declarations| B[Write a @mixin] A -->|A single computed value| C[Write a @function] B --> B1["@include it into a selector"] C --> C1[Use its return value in a property]
MixinFunction
ProducesCSS declarationsOne value
Invoked with@include name(...)name(...) inline
Ends withdeclarations / @content@return
Use forreusable style blocks, media queriesmath, color, unit conversions

Operations & the Math Module

Sass can do arithmetic on numbers, colors, and strings — the foundation of consistent spacing and type scales.

Math operations

@use 'sass:math';

$base-spacing: 8px;

.container {
  padding: $base-spacing * 2;                  // 16px
  margin-bottom: $base-spacing * 3;            // 24px
  height: calc(100vh - #{$base-spacing * 8});  // interpolate into calc()
  line-height: math.div(24px, 16px);           // 1.5
}

⚠️ Division uses math.div(), not /

The / character is being removed as a division operator because it's ambiguous with CSS shorthand like font: 16px/1.5. Always divide with the math module:

@use 'sass:math';

// ❌ deprecated
$r: 24px / 16px;

// ✅ correct
$r: math.div(24px, 16px);

A spacing scale from one unit

$unit: 8px;

$space-xs:  $unit * 0.5;  // 4px
$space-sm:  $unit;        // 8px
$space-md:  $unit * 2;    // 16px
$space-lg:  $unit * 3;    // 24px
$space-xl:  $unit * 4;    // 32px

📖 Real-world: a modular type scale

Design systems build font sizes from a ratio (1.25 = "major third"). Functions plus the math module make it one clean expression:

@use 'sass:math';

$base-font: 16px;
$ratio: 1.25;

@function type-scale($level) {
  @return math.pow($ratio, $level) * $base-font;
}

$type-base: type-scale(0);  // 16px
$type-lg:   type-scale(2);  // 25px
$type-xl:   type-scale(3);  // ~31.25px

h1 { font-size: type-scale(4); } // ~39px

Control Directives & Loops

Control directives bring programming logic into your stylesheet, letting you generate many rules from a little code.

@if / @else

@use 'sass:color';

@mixin text-contrast($bg) {
  @if color.channel($bg, 'lightness', $space: hsl) > 60% {
    color: #333;   // dark text on light backgrounds
  } @else {
    color: #fff;   // light text on dark backgrounds
  }
}

.alert--warning { background: #ffc107; @include text-contrast(#ffc107); }
.alert--danger  { background: #dc3545; @include text-contrast(#dc3545); }

@for loops

@use 'sass:math';

// A simple 12-column grid
@for $i from 1 through 12 {
  .col-#{$i} {
    width: math.percentage(math.div($i, 12));
  }
}

@each loops

The workhorse for turning a map into utility classes:

$colors: (
  'primary': #3498db,
  'success': #2ecc71,
  'danger':  #e74c3c
);

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

@while loops

$i: 1;
@while $i <= 5 {
  .opacity-#{$i * 10} { opacity: math.div($i, 10) + 0.5; }
  $i: $i + 1;
}

💡 This is how utility frameworks are built

Tailwind-style toolkits are essentially large @each/@for loops over maps of colors, spacings, and sizes. Learn the loop and you understand how thousands of utility classes are generated from a few dozen lines.

Putting It All Together

Here's a compact system where variables, a function, a mixin, and an @each loop cooperate to generate a full button family:

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

// 1) Tokens
$colors: (
  'primary': #4e73df,
  'success': #1cc88a,
  'danger':  #e74a3b
);
$spacer: 1rem;

// 2) A function to fetch a color
@function brand($key) { @return map.get($colors, $key); }

// 3) A shared base mixin
@mixin btn-base {
  display: inline-block;
  padding: ($spacer * 0.5) $spacer;
  border: 1px solid transparent;
  border-radius: 0.35rem;
  transition: background-color 0.15s ease-in-out;
  cursor: pointer;
}

// 4) Generate every variant with a loop
.btn {
  @include btn-base;

  @each $name, $value in $colors {
    &--#{$name} {
      color: white;
      background-color: $value;
      &:hover { background-color: color.adjust($value, $lightness: -10%); }
    }
  }
}

Produces, among others:

.btn { display:inline-block; padding:.5rem 1rem; border-radius:.35rem; /* … */ }
.btn--primary { color:white; background-color:#4e73df; }
.btn--primary:hover { background-color:#3a5fd0; }
.btn--success { color:white; background-color:#1cc88a; }
.btn--danger  { color:white; background-color:#e74a3b; }

Hands-on Exercise

🏋️ Build a Breakpoint Mixin and a Utility Generator

Objective: Combine a map, a @content mixin, and an @each loop.

Instructions:

  1. Define a $breakpoints map with sm, md, and lg keys.
  2. Write a respond-to($name) mixin using map.get and @content that emits a min-width media query.
  3. Define a $spaces map (e.g. '1': 4px, '2': 8px, '3': 16px).
  4. Use @each to generate .mt-1, .mt-2, .mt-3 margin-top utilities.
  5. Apply respond-to('md') inside a component to prove the mixin works.
💡 Hint

Remember to @use 'sass:map'; at the top so map.get is available. The @content keyword goes inside the @media block. Interpolate the loop key into the class name with #{$key}.

✅ Example solution
@use 'sass:map';

$breakpoints: ('sm': 576px, 'md': 768px, 'lg': 992px);

@mixin respond-to($name) {
  @media (min-width: map.get($breakpoints, $name)) {
    @content;
  }
}

$spaces: ('1': 4px, '2': 8px, '3': 16px);

@each $key, $value in $spaces {
  .mt-#{$key} { margin-top: $value; }
}

.hero {
  padding: 16px;
  @include respond-to('md') {
    padding: 48px;
  }
}

The loop emits three utility classes, and .hero gains extra padding at 768px and up.

🎯 Quick Quiz

Question 1: You need to reuse a chunk of CSS declarations across several selectors. Which tool fits?

Question 2: Which is the correct, non-deprecated way to divide in modern Sass?

Question 3: What is @content used for inside a mixin?

Summary & Quiz

🎉 Key Takeaways

  • Mixins output reusable CSS and accept parameters, defaults, variable args, and @content blocks.
  • Functions @return a single value; reach for them for math, color, and unit conversions.
  • Use the sass:math and sass:color modules — math.div() and color.adjust()/color.scale() replace the deprecated / and darken()/lighten().
  • Control directives@if, @for, @each, @while — generate CSS programmatically.
  • Combined, these features are exactly how design systems and utility frameworks are built.

📚 Further Reading

🚀 What's Next?

You can now generate and compute styles. The next lesson covers inheritance and the @extend directive — sharing a set of styles between selectors by relationship, and how @extend differs from a mixin in both syntax and compiled output.

🎉 You've leveled up!

Mixins, functions, and loops are the engine room of Sass. With these, you can build systems, not just stylesheets.