A solution for overriding Sass mixins

July 27, 2020

Have you ever found yourself needing to override a Sass mixin from one file to another?

This is a problem I found myself trying to solve recently as part of a team building a design system with a parent and child theme relationship. I couldn’t find the answer I was looking for with a Google search, so I’m documenting the experience in case it’s helpful to others.

The Problem

In this problem, the parent theme has two Sass mixins defined for two different button styles, primary and secondary.

A style rule is written to include the primary button mixin in a button selector, .component-button.

parent-theme/sass/buttons.scss

@mixin primary-button {
  background: teal;
  border: none;
  border-radius: 5px;
  color: white;
  padding: 10px;
  cursor: pointer;
}

@mixin secondary-button {
  background: red;
  border: none;
  border-radius: 10px;
  color: black;
  padding: 15px;
  cursor: pointer;
}

.component-button {
   @include primary-button();
}

The child theme inherits its button styles directly from the parent theme using Sass imports.

child-theme/sass/buttons.scss

@import './parent-theme/sass/buttons';

In the child theme, .component-button needs to use the secondary button style instead of the primary button style defined in the parent theme.

Solution #1 (Works, but not ideal)

One solution could be to write a new style rule in the child theme to override the mixin style in the parent theme.

child-theme/sass/buttons.scss

@import './parent-theme/sass/buttons';

.component-button {
    @include secondary-button();
}

This technically works, but it’s not ideal.

When the Sass get compiled into CSS it will render out both style rules in the child theme.

child-theme/css/buttons.scss

.component-button {
  background: teal;
  border: none;
  border-radius: 5px;
  color: white;
  padding: 10px;
  cursor: pointer;
}

.component-button {
  background: red;
  border: none;
  border-radius: 10px;
  color: black;
  padding: 15px;
  cursor: pointer;
}

This leads to unnecessary code bloat, which in my opinion is not a sustainable pattern. When used repeatedly, this can have a negative impact on site performance and also makes developer debugging more difficult.

Solution #2 (May work in some cases, but not this one)

Another possible solution is to re-define mixins in the child theme when they need to be overridden.

This can work if every instance where @include primary-button() is referenced should be replaced by the secondary button, but that is not the case for this problem. We’re not trying to override primary button everywhere, only in the .component-button selector for now, so this solution won’t work.

Solution #3 (Works, sustainable)

A third solution - and my favorite - is to create a new mixin to handle any case where we want to swap out one button for another in the child theme.

The mixin pattern looks like this:

@mixin button-swapper($buttonType: primary-button) {
    @if $buttonType==primary-button {
        @include primary-button();
    }
    @else if $buttonType==secondary-button {
        @include secondary-button();
    }
}

It may remind you of a JavaScript function that takes in an argument, $buttonType which is represented by a Sass variable. The body of the mixin has conditions for which button mixin to include depending on the value that’s assigned to $buttonType.

To put this pattern to use, a new mixin called button-swapper is added to the parent theme and is assigned a default variable to pass into the mixin for .component-button specifically.

parent-theme/sass/buttons.scss

@mixin primary-button {
  background: teal;
  border: none;
  border-radius: 5px;
  color: white;
  padding: 10px;
  cursor: pointer;
}

@mixin secondary-button {
  background: red;
  border: none;
  border-radius: 10px;
  color: black;
  padding: 15px;
  cursor: pointer;
}

@mixin button-swapper($buttonType: primary-button) {
    @if $buttonType==primary-button {
        @include primary-button();
    }
    @else if $buttonType==secondary-button {
        @include secondary-button();
    }
}

$component-button-type: primary-button !default;
.component-button {
    @include button-swapper($component-button-type);
}

Now the only thing needed in the child theme is a reassignment of the $component-button-type variable to the secondary button.

child-theme/sass/buttons.scss

@import './parent-theme/sass/buttons';

$component-button-type: secondary-button;

The CSS output this produces is much cleaner than the original solution.

child-theme/css/buttons.scss

.component-button {
  background: red;
  border: none;
  border-radius: 10px;
  color: black;
  padding: 15px;
  cursor: pointer;
}

Not only is the CSS output cleaner (yay performance! yay debugging!), but this solution also allows for selector specific targeting to change the button type on as-needed basis. This can make the overall design system more flexible and maintainable.

If you’d like to see this mixin in action, check out this CodePen:

Wrap-up

Leveraging a clever combination of Sass variables and mixins can be useful in creating a pattern for swapping out reusable styles where needed in child themes. This pattern can be extended for other similar style mixins, such as heading styles or iconography.

As design systems grow in complexity the need for flexibility also increases.

Overriding mixins in Sass is tricky, but not impossible!


Tammy Ritterskamp

Hey there! I'm a front-end web developer living and working in St. Louis, MO. Let's connect in these social places: