⚙️ 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
@contentblocks - Create custom functions and distinguish clearly when to use a function vs a mixin
- Use the
sass:mathandsass:colormodules 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.
| Mixin | Function | |
|---|---|---|
| Produces | CSS declarations | One value |
| Invoked with | @include name(...) | name(...) inline |
| Ends with | declarations / @content | @return |
| Use for | reusable style blocks, media queries | math, 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:
- Define a
$breakpointsmap withsm,md, andlgkeys. - Write a
respond-to($name)mixin usingmap.getand@contentthat emits amin-widthmedia query. - Define a
$spacesmap (e.g.'1': 4px, '2': 8px, '3': 16px). - Use
@eachto generate.mt-1,.mt-2,.mt-3margin-top utilities. - 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
@contentblocks. - Functions
@returna single value; reach for them for math, color, and unit conversions. - Use the
sass:mathandsass:colormodules —math.div()andcolor.adjust()/color.scale()replace the deprecated/anddarken()/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.