SCSS transition mixin for BEM-element

23 July 2017

How to improve element interaction SCSS styles coding and be more efficient.

An example

Static element

Interactive element

If you need to work hard and fast you might need to optimize the development process routine. Today we’ll optimize properties allocation transitions of BEM-elements of their different states (:hover, :focus, .is-active, and so on).

So, let’s see the example of an element. Doesn’t matter it’s far from real life, it’s just an example.

Here is the ready SCSS required for styles modification on a hover event:

.example-block-interactive {
    margin: 1em;
    padding: 1em;
    background-color: cadetblue;
    color: #fff;
    text-align: center;
    transition: background-color 0.25s;

    &:hover {
        box-shadow: 0 4px 10px 0 rgba(0,0,0,.1), 0 2px 4px 0 rgba(0,0,0,.2);

        &::before {
            box-shadow: 0 4px 10px 0 rgba(0,0,0,.1), 0 2px 4px 0 rgba(0,0,0,.2);
        }

        &::after {
            background-color: darkcyan;
            max-width: 100%;
        }
    }

    &:hover & {
        &__title {
            transform: translateY(-0.25em);
        }
    }

    &::before {
        content: "";
        position: absolute;
        right: 0.25em;
        bottom: 100%;
        left: 0.25em;
        height: 2px;
        background-color: inherit;
        margin-bottom: 0.1em;
        transition: box-shadow 0.25s;
    }


    &::after {
        content: "";
        display: block;
        height: 2px;
        max-width: 0;
        margin: auto;
        transition: max-width 0.5s ease-out, background-color 0.25s;
    }

    &__title {
        transition: transform 0.25s;
    }
}

There is the CSS we get for using in a browser:

.example-block-interactive {
  margin: 1em;
  padding: 1em;
  background-color: cadetblue;
  color: #fff;
  text-align: center;
  transition: background-color 0.25s;
}
.example-block-interactive:hover {
  box-shadow: 0 4px 10px 0 rgba(0,0,0,.1), 0 2px 4px 0 rgba(0,0,0,.2);
}
.example-block-interactive:hover::before {
  box-shadow: 0 4px 10px 0 rgba(0,0,0,.1), 0 2px 4px 0 rgba(0,0,0,.2);
}
.example-block-interactive:hover::after {
  background-color: darkcyan;
  max-width: 100%;
}
.example-block-interactive:hover .example-block-interactive__title {
  transform: translateY(-0.25em);
}
.example-block-interactive::before {
  content: "";
  position: absolute;
  right: 0.25em;
  bottom: 100%;
  left: 0.25em;
  height: 2px;
  background-color: inherit;
  margin-bottom: 0.1em;
  transition: box-shadow 0.25s;
}
.example-block-interactive::after {
  content: "";
  display: block;
  height: 2px;
  max-width: 0;
  margin: auto;
  transition: max-width 0.5s ease-out, background-color 0.25s;
}
.example-block-interactive__title {
  transition: transform 0.25s;
}

Problem

To avoid tons of mistakes we might have doing this manually let’s use a SCSS mixin which allows:

  • Get the pairs as “property: its active value”;
  • Allows to change “transition-duration” and “transition-easing” properties individually for each other property;
  • Allows to change the element itself, its pseudo-elements or child elements;
  • Allows to group properties changing for a few elements at once;
  • Allows to change states list of the element and their modifications.

Solution

And here is our ready to use SCSS mixin:

@mixin property-transition(
    $selectors,
    $properties,
    $duration: 0.25s,
    $easing: ease,
    $states: ":hover"
) {
    $transition-selector: null;
    $active-selector: null;
    $transition-property: null;

    @each $selector in $selectors {
        $transition-selector: $transition-selector, & + $selector;

        @if (str-slice($selector, 0, 5) == ":self") {
            $transition-selector: &;
            @each $state in $states {
                $active-selector: $active-selector, & + $state;
            }
        } @else if (str-slice($selector, 0, 1) == ":") {
            @each $state in $states {
                $active-selector: $active-selector, & + $state + $selector;
            }
        } @else {
            @each $state in $states {
                $active-selector: $active-selector, & + $state & + $selector;
            }
        }
    }

    @each $property, $value in $properties {
        $item-duration: $duration;
        $item-easing: $easing;

        @if type-of($value) == "map" {
            $item-duration: map-get($value, "duration");
            $item-easing: map-get($value, "easing");
        }

        $transition-property: #{$transition-property, $property $item-duration $item-easing};
    }

    @at-root #{$transition-selector} {
        transition: $transition-property;
    }

    @at-root #{$active-selector} {
        @each $property, $value in $properties {
            @if type-of($value) == "map" {
                $value: map-get($value, "value");
            }

            #{$property}: $value;
        }
    }
}

The mixin takes 5 arguments:

1. Selectors for using with the “transition” property.

That might be a string on an array of the selectors.

  • “:self” – change the properties of the element itself;
  • pseudo-elements;
  • any child element which has a parent class as a base.

2. The map of the properties and their values for the active element’s state.

If you don’t need to change the “transition-duration” or “transition-easing” properties then just pass the required value only:

@include property-transition(
    "__title",
    (
        color: red
    )
);

Otherwise, just pass the arguments as a map:

@include property-transition(
    "__title",
    (
        color: (
            value: red,
            duration: 0.5s,
            easing: ease-out
        )
    )
);

3. Common “transition-duration” property for all passed properties.

4. Common “transition-easing” property for all passed properties.

5. States list which will be used for styles changing.

For example, if you want to change the styles not only for “:hover” state, but for the “:focus” state too, or if the element has a “.is-active” class, then set the argument value to “:hover” “:focus” “.is-active”.

Let’s use the mixin for our example:

.example-block-interactive {
    margin: 1em;
    padding: 1em;
    background-color: cadetblue;
    color: #fff;
    text-align: center;

    @include property-transition(
        (
            ":self",
            "::before"
        ),
        (
            box-shadow: #{0 4px 10px 0 rgba(0,0,0,.1), 0 2px 4px 0 rgba(0,0,0,.2)}
        )
    );

    @include property-transition(
        "::after",
        (
            background-color: darkcyan,
            max-width: (
                value: 100%,
                duration: 0.5s,
                easing: ease-out
            )
        )
    );

    @include property-transition(
        "__title",
        (
            transform: translateY(-0.25em)
        )
    );

    &::before {
        content: "";
        position: absolute;
        right: 0.25em;
        bottom: 100%;
        left: 0.25em;
        height: 2px;
        background-color: inherit;
        margin-bottom: 0.1em;
    }

    &::after {
        content: "";
        display: block;
        height: 2px;
        max-width: 0;
        margin: auto;
    }
}

Using the mixin this is no necessary to define the transition property for the child elements. You need to describe only the required properties and their changed values – the mixin will make the other work.

Here we have a tool that can save a few minutes per day for us. We can use this spare time for ping-pong play and disco dance 🙂 Happy coding!

1