Mixins

This section documents Sass/SCSS as implemented by Dart Sass, the current official and actively maintained compiler — it is not tied to any specific book, build tool, or CSS framework (Bootstrap, Bulma, etc.). Unlike the other reference sections on this site, no single reference book underpins it: the content was generated with the assistance of AI from general knowledge of Dart Sass, and should be verified against the current official documentation at sass-lang.com before relying on it in production. Sass continues to evolve, so behaviour described here may lag the compiler you are actually running.

This section’s bibliography lists the reference material consulted while preparing these pages.

A mixin is a named, reusable group of CSS declarations (and optionally whole rules) that can be dropped into any selector. Mixins are the main tool for eliminating repetition in a stylesheet.

Defining and including

@mixin defines; @include uses.

@mixin visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link:not(:focus) {
  @include visually-hidden;
}
.skip-link:not(:focus) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

Mixin names, like variable names, treat hyphens and underscores as equivalent. A mixin taking no arguments may be included with or without empty parentheses (@include visually-hidden; or @include visually-hidden();).

A mixin may contain complete rules, not just declarations — including nested selectors and media queries:

@mixin card-surface {
  background: white;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.2);

  &:hover { box-shadow: 0 4px 12px rgb(0 0 0 / 0.25); }

  @media (prefers-color-scheme: dark) {
    background: #1e1e1e;
  }
}

Arguments

Arguments make a mixin parameterised. They are declared as variables in parentheses and are in scope for the mixin’s body:

@mixin square($size, $colour) {
  width: $size;
  height: $size;
  background: $colour;
}

.avatar { @include square(48px, #3f51b5); }
.thumb  { @include square(96px, #e91e63); }

Named arguments

Arguments can be passed by name instead of position. This makes a long call self-documenting and lets you skip over defaults you don’t want to change:

@mixin button($bg, $fg: white, $radius: 4px, $padding: 8px 16px) {
  background: $bg;
  color: $fg;
  border-radius: $radius;
  padding: $padding;
  border: 0;
}

.btn-primary { @include button(#3f51b5); }
.btn-pill    { @include button(#e91e63, $radius: 999px); }
.btn-ghost   { @include button($bg: transparent, $fg: #3f51b5); }

Named arguments must come after any positional ones. Because they are matched by name, renaming a mixin’s parameter is a breaking change for its callers.

Default values

A default is an expression, evaluated when the mixin is called, and it may refer to earlier parameters of the same mixin:

@mixin inset($top, $right: $top, $bottom: $top, $left: $right) {
  top: $top;
  right: $right;
  bottom: $bottom;
  left: $left;
}

.overlay { @include inset(0); }          // all four sides 0
.panel   { @include inset(8px, 16px); }  // 8/16/8/16

Arbitrary arguments

A trailing parameter written $name…​ collects every remaining argument into a list — Sass’s variadic form. This is how a mixin wraps a CSS property that itself takes a variable number of comma-separated values:

@mixin shadows($shadows...) {
  box-shadow: $shadows;
}

.card {
  @include shadows(0 1px 2px rgb(0 0 0 / 0.1), 0 4px 12px rgb(0 0 0 / 0.15));
}

If named arguments are passed to a variadic mixin, they are collected into a map accessible via meta.keywords():

@use "sass:meta";

@mixin custom-properties($args...) {
  @each $name, $value in meta.keywords($args) {
    --#{$name}: #{$value};
  }
}

:root {
  @include custom-properties($primary: #3f51b5, $radius: 4px);
}
:root {
  --primary: #3f51b5;
  --radius: 4px;
}

The same …​ syntax works at the call site, spreading a list or map into individual arguments:

$brand-shadow: (0 1px 2px rgb(0 0 0 / 0.1), 0 4px 12px rgb(0 0 0 / 0.15));
.card { @include shadows($brand-shadow...); }

$btn-config: (bg: #3f51b5, radius: 999px);
.btn { @include button($btn-config...); }   // map keys become named arguments

Content blocks with @content

@content marks the place where a block passed by the caller is injected. This turns a mixin into a wrapper — the caller supplies the styles, the mixin supplies the context they go in.

The canonical use is a media-query helper:

@use "sass:map";

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

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

  @if $width == null {
    @error "Unknown breakpoint `#{$name}`. Expected one of: #{map.keys($breakpoints)}.";
  }

  @media (min-width: $width) {
    @content;
  }
}

.sidebar {
  width: 100%;

  @include respond-to(md) {
    width: 280px;
    float: left;
  }

  @include respond-to(lg) {
    width: 320px;
  }
}
.sidebar { width: 100%; }
@media (min-width: 768px) {
  .sidebar { width: 280px; float: left; }
}
@media (min-width: 992px) {
  .sidebar { width: 320px; }
}

A mixin may use @content more than once, emitting the block in several places, and a mixin with no block passed simply produces nothing where @content appears.

Passing arguments to a content block

@content(…​) passes values out to the block, which receives them with using ($var):

@use "sass:map";

$themes: (
  light: (bg: #ffffff, fg: #212121),
  dark:  (bg: #121212, fg: #eeeeee)
);

@mixin each-theme {
  @each $name, $colours in $themes {
    [data-theme="#{$name}"] & {
      @content($colours);
    }
  }
}

.panel {
  @include each-theme using ($c) {
    background: map.get($c, bg);
    color: map.get($c, fg);
  }
}
[data-theme="light"] .panel { background: #ffffff; color: #212121; }
[data-theme="dark"] .panel { background: #121212; color: #eeeeee; }

Note that a content block is evaluated in the lexical scope where it was written, not inside the mixin. It can therefore see the caller’s variables, but not the mixin’s local ones — which is exactly why using exists.

Mixins vs. functions vs. @extend

  • A mixin emits declarations. Reach for it when you want to inject styles.

  • A function returns a single value and emits nothing — see Functions.

  • @extend merges selectors instead of duplicating declarations — see Selector Inheritance for the trade-off between the two.

The practical difference between a mixin and @extend: a mixin copies its declarations into every caller, so ten callers produce ten copies (which gzip compresses well, and which keeps the cascade predictable), whereas @extend produces one rule with ten selectors on it.