0

This question is mainly about readability and understanding of the code. Im am also in the process of creating a SCSS framework like Compass and Bourbon.

I struggle to write SCSS because I like to see my own code as clean and reusable as possible. I don't have this issue when writing code for a real programmming language, like PHP, JavaScript or Java. It is just the evil CSS and SCSS that is annoying me in my recent projects.

So, I have some SCSS mixins whose purpose is basically behave as shortcuts for default CSS syntax. My idea was to avoid repetition and improve speed when implementing any desing. For example, I have some mixins that behaves as follows:

// Mixin for margin and padding (I took it from Bourbon)
html {
    @include margin(1rem null 1rem 0.5rem);
}
// OUTPUT
html {
  margin-bottom: 1rem;
  margin-left: 0.5rem;
  margin-top: 1rem;
}


// Mixin for media queries
@include media(min-width 300, max-width 600, portrait) {
    font-size: 12px;
}
// OUTPUT
@media screen and (min-width: 300px) and (max-width: 599px) and (orientation: portrait) {
  html {
    font-size: 12px;
  }
}


// Mixin for transition
html {
    @include transition(transform opacity, 1s, 0.5s 0);
}
// OUTPUT
html {
  transition-delay: 0.5s, 0;
  transition-duration: 1s;
  transition-property: transform, opacity;
  transition-timing-function: ease;
}

The code for the last example, hiding some helper functions, is this:

// this function is used in other places
@function map-value($map, $value, $keys...) {
    @each $key in $keys {
        @if map.get($map, $key) == null {
            @return map.merge($map, (#{$key}: if(type-of($value) == list, lst.join((), $value, comma), $value)));
        }
    }
    @return $map;
}

@function map-transition($args) {
    $result: ();

    @each $list in $args {
        @if each-is($list, 'number') {
            $result: map-value($result, $list, 'duration', 'delay');
        }
        @else if each-is($list, 'string') {
            $result: map-value($result, $list, 'property', 'timing-function');
        }
    }
    @return map.merge(('delay': null, 'duration': null, 'property': null, 'timing-function': ease), $result);
}

@mixin transition($args...) {
    @each $key, $value in map-transition($args) { transition-#{$key}: $value; }
}

I also have mixins for animation, border, position, outline, etc, all shortcuts for the default CSS syntax. I will refer to them as shortcut mixins.

The Question Is

What is the point of doing it? I waste a couple of seconds typing default CSS declaration more than using these mixins. Are these mixins worth it? Why?

There are mixins that actually do something, because they encapsulate some pattern. But these shortcuts mixins are not encapsulating anything, just making me type less, at the price of clarity, maybe. What do you think?

Let's say you have to make a serie of adjustments in a CSS with more than 10.000 lines, made by someone, and you find the source code for it. Then you see that there is more than 100 SCSS files in a folder called 'components'.

Would you prefer to see the components files using default CSS instead of shortcut mixins? Why? Does it help with readability and understanding of the code? Basically, should I keep using and writing shortcut mixins besides mixins that actually do something? Why?

This may seem as a opinative question, but your answer is very important to me, because it will help me to know if i am in the right path to writing reusable, clean and "pretty" SCSS code.

1 Answer 1

1

I think you answered it yourself:

But these shortcuts mixins are not encapsulating anything, just making me type less, at the price of clarity, maybe

I like introducing abstractions. But not every abstraction is good. Every abstraction introduces a bit of complexity:

  • The abstraction itself is typically more complex than any concrete occurrence of the pattern which you abstracted away.
  • Each time the abstraction is used, there is a bit of cognitive overhead: to understand the code where the abstraction is used, you need to understand what the abstraction does.

When I look at your example @include margin(1rem null 1rem 0.5rem); – surely there was some complicated stuff involved that made it worth extracting. It can't just be a margin: ... property, right?

So it expands to a couple of lines like margin-left: 0.5rem. The value provided by this abstraction is that it has syntax similar to the margin shorthand property, but allows a null argument to inherit/cascade a value for one side.

Personally, I find this a bit surprising and not worth the abstraction. However, in a large code base where this pattern “margin except cascade one side” occurs very often, any reader would only have to learn about this pattern once and could then quickly see what is going on. If it were not for that null behaviour, this abstraction would have no apparent use though.

When I look at your transition() functions, I notice that this mixin involves a lot of complexity. It is not at all obvious how it works, even though the usage is reasonably clear. But unless the usage of this mixin is frequent, using the mixin is more difficult to understand because context like duration or delay is missing. The implementation of these functions is also likely to increase total complexity across the project. I'd rather type out the transition-* properties seven times or so.

The best abstractions are those that simplify the overall code because they introduce more context than there would be without them. Abstractions can give a name to a repeated pattern, but also to an individual part of the code. Good names typically relate to the business purpose of this code, not to the implementation choices. Using this heuristic, a margin() mixin is questionable because it lives on the level of CSS implementation. In contrast, a name that lives on the level of the layout or design of a component might be:

@mixin tile-margin($base-margin) {
  margin-top: $base-margin;
  margin-bottom: $base-margin;
  margin-left: 0.5 * $base-margin;
  // no margin-right so that there is no extra margin between tiles
}

This not only encapsulates the pattern “one side is missing”, but also a pattern of how the margins on these sides relate to each other.

In the end, choosing the right names and the right abstractions is not an exact science. More of an art. A lot of it is due to personal preference. A lot also comes with experience, and the best way to get that experience is to make a decision, use it for a while, and later discover that you probably made the wrong choice. Learning from your own mistakes is best, though suffering the excessive abstractions or lack of abstractions in other people's code also has didactic value. One of my personal projects right now is rewriting a tool that I wrote ~6 years ago. I'm really happy about some choices I made, but also really happy when I can now see how my past self made some really questionable design choices.

2
  • Thank you for the answer. I literally took notes. So, are you saying that most of the abstractions should live on the level of the layout or design of a component and introduce some context right? How much generic should an abstraction be? Should I worry about creating reusable code to use across multiple projects? You know, it is a habit of some programmers try to create code that is as generic as possible. Could it be that my mistake was making abstractions too generic?
    – Pagel
    Dec 2, 2021 at 22:19
  • @Pagel the problem with very generic abstractions is that the more generic they are, the less they abstract over: at some point, using the abstraction takes about as much effort as the non-abstract approach. So when building abstractions, it's good to keep YAGNI in mind: you're probably not going to need an ultra-generic approach. Instead, it often makes sense to develop abstractions bottom-up: refactor to an abstraction when you notice a repeated pattern. But there also are cases where solving multiple smaller abstract problems is simpler than solving a large concrete problem.
    – amon
    Dec 3, 2021 at 10:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.