Type something to search...
Sass Mixins and Functions: Writing DRY Stylesheets

Sass Mixins and Functions: Writing DRY Stylesheets

Every stylesheet eventually grows a little copy-paste problem. The same breakpoint query shows up in forty components. The same five lines of "visually hidden" CSS get pasted into every project. A spacing value gets calculated by hand, slightly differently, in six places. It works, until you need to change it.

Sass has two tools built for exactly this: mixins and functions. Both let you write logic once and reuse it everywhere, which is the heart of the DRY principle: Don't Repeat Yourself. They look similar at first, but they do different jobs, and knowing which one to reach for makes your code cleaner and your compiled CSS smaller.

In this guide, I'll cover how mixins and functions work in modern Sass, including the module system with @use, walk through practical examples you can drop into a project, and flag the habits that quietly bloat your output.

Mixins vs. Functions: The One-Sentence Difference

  • A mixin outputs declarations or rules. You @include it, and CSS appears.
  • A function returns a value. You call it inside a property, and it computes something.
@use "sass:math";

// Mixin: outputs declarations
@mixin flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

// Function: returns a value
@function rem($px, $base: 16px) {
  @return math.div($px, $base) * 1rem;
}

.hero {
  @include flex-center;
  padding: rem(48px);
}

If you want a chunk of CSS, write a mixin. If you want a number, color, string, or list, write a function.

Setting Up: Modern Sass with @use

Before diving in, a quick note on setup. The current Sass implementation is Dart Sass (the sass package on npm). Older tutorials use @import and global functions like map-get() or darken(). Those are deprecated in Dart Sass, and @import is scheduled for removal in a future major version, so new code should use the module system.

npm install --save-dev sass
// Load built-in modules
@use "sass:math";
@use "sass:map";
@use "sass:color";
@use "sass:list";
@use "sass:meta";

// Load your own partials
@use "abstracts/mixins" as mx;
@use "abstracts/functions" as fn;

With @use, members are namespaced by default. A mixin called respond in abstracts/_mixins.scss becomes mx.respond (or mixins.respond without an alias). That removes naming collisions and makes it obvious where everything comes from.

A typical folder layout:

scss/
├── abstracts/
│   ├── _tokens.scss
│   ├── _functions.scss
│   ├── _mixins.scss
│   └── _index.scss
├── components/
│   ├── _button.scss
│   └── _card.scss
└── main.scss

An _index.scss file can forward everything so components need just one import:

// abstracts/_index.scss
@forward "tokens";
@forward "functions";
@forward "mixins";
// components/_button.scss
@use "../abstracts" as *;

The as * removes the namespace. Use it sparingly, for your own well-known abstracts, not for third-party libraries.

Writing Mixins

Basic Mixins

A mixin is a named block of styles:

@mixin visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link:not(:focus) {
  @include visually-hidden;
}

Arguments and Defaults

Mixins accept arguments, with optional defaults:

@mixin truncate($lines: 1) {
  overflow: hidden;

  @if $lines == 1 {
    white-space: nowrap;
    text-overflow: ellipsis;
  } @else {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: $lines;
  }
}

.card-title {
  @include truncate;
}

.card-excerpt {
  @include truncate(3);
}

You can also pass arguments by name, which makes calls with several options much easier to read:

@mixin button-variant($bg, $color: #fff, $radius: 8px) {
  background: $bg;
  color: $color;
  border-radius: $radius;

  &:is(:hover, :focus-visible) {
    background: color.adjust($bg, $lightness: -8%);
  }
}

.btn-primary {
  @include button-variant(#6366f1);
}

.btn-pill {
  @include button-variant(#0ea5e9, $radius: 999px);
}

Passing Content with @content

@content lets a mixin wrap a block of styles you pass in. This is how the most useful mixin of all, the responsive breakpoint helper, works:

// abstracts/_tokens.scss
$breakpoints: (
  "sm": 640px,
  "md": 768px,
  "lg": 1024px,
  "xl": 1280px,
);
// abstracts/_mixins.scss
@use "sass:map";
@use "tokens";

@mixin respond($size) {
  @if not map.has-key(tokens.$breakpoints, $size) {
    @error "Unknown breakpoint `#{$size}`. Use one of: #{map.keys(tokens.$breakpoints)}.";
  }

  @media (min-width: map.get(tokens.$breakpoints, $size)) {
    @content;
  }
}
.product-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;

  @include respond("md") {
    grid-template-columns: repeat(2, 1fr);
  }

  @include respond("lg") {
    grid-template-columns: repeat(4, 1fr);
  }
}

Compiled output:

.product-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}
@media (min-width: 768px) {
  .product-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 1024px) {
  .product-grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

Now every breakpoint in the project lives in one map. Change md from 768px to 800px, and the whole site updates. The @error also catches typos at compile time instead of silently producing broken CSS.

Other Handy @content Mixins

@mixin hover-supported {
  @media (hover: hover) and (pointer: fine) {
    &:hover {
      @content;
    }
  }
}

@mixin dark {
  @media (prefers-color-scheme: dark) {
    @content;
  }
}

@mixin reduced-motion-safe {
  @media (prefers-reduced-motion: no-preference) {
    @content;
  }
}

.card {
  transition: transform 0.2s;

  @include hover-supported {
    transform: translateY(-2px);
  }

  @include dark {
    background: #1f2937;
  }
}

Variable Arguments

Use ... to accept any number of arguments. This is great for properties that take comma-separated lists:

@mixin transition($props...) {
  $list: ();

  @each $prop in $props {
    $list: list.append($list, $prop 0.2s ease, $separator: comma);
  }

  transition: $list;
}

.link {
  @include transition(color, background-color, border-color);
}

Output:

.link {
  transition:
    color 0.2s ease,
    background-color 0.2s ease,
    border-color 0.2s ease;
}

Writing Functions

Functions are declared with @function and must @return a value.

Converting Units

// abstracts/_functions.scss
@use "sass:math";

@function rem($px, $base: 16px) {
  @if math.unit($px) != "px" {
    @error "rem() expects a px value, got `#{$px}`.";
  }

  @return math.div($px, $base) * 1rem;
}
.title {
  font-size: rem(28px); // 1.75rem
  margin-bottom: rem(12px); // 0.75rem
}

Note the use of math.div(). Using / for division in Sass is deprecated, because in plain CSS / is also a separator, as in grid-area: 1 / 3 or aspect-ratio: 16 / 9.

A Spacing Scale

A function makes it easy to keep spacing consistent:

@use "sass:math";

$space-unit: 4px;

@function space($step) {
  @return math.div($step * $space-unit, 16px) * 1rem;
}

.card {
  padding: space(6); // 1.5rem
  gap: space(3); // 0.75rem
}

Reading Design Tokens

Functions are the ideal way to read values from maps, with a helpful error if a key doesn't exist:

@use "sass:map";

$colors: (
  "primary": #6366f1,
  "accent": #f472b6,
  "success": #34d399,
  "warning": #fbbf24,
  "ink": #0f172a,
);

@function color($name) {
  @if not map.has-key($colors, $name) {
    @error "Unknown color `#{$name}`.";
  }

  @return map.get($colors, $name);
}

.alert-success {
  border-left: 4px solid color("success");
}

Fluid Type with clamp()

Functions can also generate CSS functions as output. Here's one that builds a fluid clamp() between two viewport widths:

@use "sass:math";

@function fluid($min-size, $max-size, $min-vw: 360px, $max-vw: 1280px) {
  $slope: math.div($max-size - $min-size, $max-vw - $min-vw);
  $intercept: $min-size - $slope * $min-vw;

  $min-rem: math.div($min-size, 16px) * 1rem;
  $max-rem: math.div($max-size, 16px) * 1rem;
  $base-rem: math.div($intercept, 16px) * 1rem;
  $growth: $slope * 100vw;

  @return clamp($min-rem, $base-rem + $growth, $max-rem);
}

h1 {
  font-size: fluid(32px, 56px);
}

Because rem and vw can't be combined at compile time, Dart Sass keeps the middle expression as a CSS calculation. The result is a single declaration along the lines of font-size: clamp(2rem, 1.41rem + 2.61vw, 3.5rem) (Sass prints more decimal places), so text scales smoothly between the two sizes while the rem parts still respect the user's font-size settings.

Color Functions

The sass:color module offers precise color manipulation:

@use "sass:color";

$brand: #6366f1;

.button {
  background: $brand;
  border-color: color.scale($brand, $lightness: -20%);

  &:hover {
    background: color.adjust($brand, $lightness: -6%);
  }
}

.button-soft {
  background: color.mix($brand, #fff, 15%);
  color: $brand;
}

color.scale() changes a value proportionally (20% of the way toward black), while color.adjust() changes it by a fixed amount. scale usually looks more natural across a palette.

That said, for colors that need to respond at runtime, such as themes or user-controlled accents, native CSS custom properties and color-mix() are often a better fit than Sass, because Sass values are fixed at compile time.

Generating Utilities with Loops

Mixins and functions really pay off when combined with @each:

@use "sass:map";

$spacing: (
  "0": 0,
  "1": 0.25rem,
  "2": 0.5rem,
  "4": 1rem,
  "8": 2rem,
);

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

  .mb-#{$key} {
    margin-bottom: $value;
  }
}

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

A dozen lines produce a consistent set of utility classes. Just be deliberate: generating hundreds of classes you never use is a fast way to ship a bloated stylesheet.

Mixins vs. @extend vs. Placeholders

You might also see @extend and placeholder selectors (%name) recommended for reuse:

%card-base {
  border-radius: 12px;
  padding: 1.5rem;
}

.card {
  @extend %card-base;
}

.panel {
  @extend %card-base;
}

@extend groups selectors together (.card, .panel { ... }) instead of duplicating declarations, which can produce smaller CSS. But it can also create surprising selector combinations, doesn't work across media queries, and makes the output harder to predict. Most teams today prefer mixins for reuse and use @extend rarely, if at all. Gzip and Brotli compression also make repeated declarations from mixins much cheaper than they look.

Common Mistakes

  • Using a mixin where a function belongs. If you only need a value, a function is simpler and can be used anywhere a value is valid.
  • Mixins that output huge blocks. Every @include copies the whole block. A 40-line mixin included 30 times adds 1,200 lines. For large shared styles, use a class instead.
  • Wrapping native CSS for no reason. You don't need a border-radius mixin or a box-shadow prefix mixin in 2026. Let Autoprefixer handle any prefixes.
  • Staying on @import and global functions. They're deprecated. Migrate with the official migrator: npx sass-migrator module --migrate-deps main.scss.
  • Silent failures. Use @error and @warn inside mixins and functions to catch bad input at compile time.
  • Doing at compile time what should be runtime. Theme colors and user preferences belong in CSS custom properties, which can change without recompiling.

Conclusion

Mixins and functions are the core of DRY Sass. Mixins output styles and shine for breakpoints, state wrappers, and repeated patterns, especially with @content. Functions return values and are perfect for unit conversions, spacing scales, token lookups, and generated clamp() values.

Write them with the modern module system, namespace them with @use, validate their input with @error, and keep each one small and focused. Your source files get shorter, your compiled CSS stays lean, and the next time a breakpoint or spacing value changes, you'll edit one line instead of forty.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading