Sass (syntactically awesome stylesheets) is a powerful tool that helps users write CSS with ease. If you’re reading this, however, you probably already knew that. In this post, I explore the more advanced possibilities of Sass. Following are seven techniques to help developers write more efficient code.
Okay, I’m lying—It’s six.
The first one is not really a technique–rather, a naming strategy. It's a key point for a good development process. However, if you already have a strategy that works better for you, feel free to share in the comments section below.
Create components
Your file structure and naming strategy determines your future development process, especially if your application grows and you are working with a team.
There are a many strategies, but after trying some of them, I’ve picked one that has worked best for us at Devbridge—components. Components are independent items that can be reused almost anywhere. There can be small, presentational components, such as a “Button”, “Input” or bigger “organisms,” such as “Article.” The key point is to make them independent: you should not create page or content-specific modules. Keep them oriented to the object, display type or function and name them so that they’re easily understood when read (avoid shorthand such as btn, frm, ppl, etc.).
For a file structure, it’s recommended you create one file per component. It might look odd at first, but it helps you find what you’re looking for much faster. Treat it like a class in OOP programming, or if you are familiar with React.JS, like a React component. You might ask, “What if I have tons of small components?” Organize them with folders. For example, put all your form components to /form-elements folder.
Another concern is styles encapsulation. In some cases, you’ll need to make changes—your button needs to be wider or bigger, for instance. Normally, you would override it by context, as in “.my-custom-page .button”. However, later that might create a mess, and you’ll have a hard time trying to find all the overrides in the whole project. Instead, you could modify the component itself: for instance, “.button--large”. Of course, it won’t save you from overrides (and the dirty nature of CSS, cascading), if you don’t follow these rules later. But rules are meant to be broken, aren’t they?
Probably the best way to keep up with this pattern is to use the BEM naming strategy of CSS classes (if you don't know what BEM is, you can read about it here). BEM plays well with Sass. Using & selector and nesting, you can construct your modules easily.
.article {
width: 100px;
&__title {
font-size: 20px;
font-weight: 500;
}
&--wide {
width: 200px;
}
}
Local variables and mixins
Variables and mixins are global in Sass. If you defined $primary-color once, you can use the value anywhere. That’s not always a good thing. For example: you have a static value (like fixed height), and you only use it for the component and its child elements. The first option (and not an efficient one) would be to hard-code the value a few times, plus another one, to create a global variable. This approach later might bloat your global variables list with code that’s too specific.
There’s also a less-used—but definitely better—option: local variables. It's possible to define a variable inside the selector's block, next to the properties. As a result, it won’t be accessible anywhere outside the selector.
The same works for mixins. Not only can you create global mixins to share code between components, you can also use local ones when you need to create repetitive stuff inside your component. A real-world example could be a need to recalculate something on every breakpoint, just with a different width/height value.
.filters-block {
$_filter-height: 20px;
@mixin _note-size($value) {
height: $value;
line-height: $value;
margin-top: -($value);
}
&__list {
height: $_filters-height * 7; // Display 7 filters
overflow: auto;
}
&__item {
height: $_filter-height;
}
&__note {
@include _note-size(30px);
}
@media screen and (max-width 1000px) {
&__note {
@include _note-size(40px);
}
}
}
Another key point is to always try to define local mixins and variables first on your new component. This keeps encapsulation and the readability of having everything component-related in one place. Move them globally only if it needs to be shared among components.
To indicate local variables, I’d recommend using underscore “_”, which comes from other programming languages, before the name of a variable/mixin.
Use maps or lists
Maps are object-like Sass variables. We use them for global layout values, z-indexes, and other global configuration values. You may ask, “Why maps and not regular variables?” Although it’s much easier to use a variable than fetch a value from the map, the key point is readability. The map is JSON-look object with possibility to nest values. Instead of a long list of global variables (which isn’t easy to read), we have an object with a logical structure.
Imagine if we had two different layouts for mobile and desktop. With variables, we might create $header-height—desktop, $header-height—mobile, etc. Not only is the naming odd, you also have to keep them grouped for readability. With maps, however, we can define two separate maps—$desktop-layout and $mobile-layout with different values—which makes your code easier to read and maintain.
$mobile-layout: (
layout-values: (
header: (
height: 72px
),
sidebar: (
width: 100%
)
)
);
$desktop-layout: (
layout-values: (
header: (
height: 86px
),
sidebar: (
width: 300px
)
)
);
The hardest part is fetching the values, especially if you use nested maps. To solve for this, you can create a function for fetching those values. It will take time at first, but all you will need to do later is to call the function. To save you time, here are a few functions that we use on our project to fetch the data from maps/lists.
Another major use case of maps (or lists) is z-index values. It’s a pain to maintain all those z-indexes of different components. You’ll end up hacking it later if your project grows. Do you have z-index: 9999 on your project? We don’t. We define all our z-indexes on a list in one place. And it’s super easy to make one component in higher order than another–just reorder the values in the map and you’ve got the result.
$z-indexes: (
error,
modal,
header,
sidebar,
footer
);
$this variable
The common issue if you are creating self-controlled components (especially if you use BEM naming) is the construction of selectors inside the modifiers. If you have a modifier that changes the properties of a child element, your code would probably look like this:
.filter-block {
&__title {
color: black;
}
&--expandable {
.filter-block__title {
color: blue;
}
}
}
There’s nothing wrong with this, but you duplicate your selector name several times. To optimize this, you can use a Javascript pattern. Remember "var self = this"? Luckily, we don’t need to do that anymore with EcmaScript 6, but it’s possible to use this pattern on Sass. You can store your parent selector as a local variable—$this: & at the top of the selector block. Ampersand in Sass returns a full selector, and in this case you lock it at the top level. Following is the same code written using this pattern:
.filter-block {
$this: &;
&__title {
color: black;
}
&--expandable {
#{$this}__title {
color: blue;
}
}
}
An important note is to keep the component at the top level. It’s not difficult to understand that if your block is nested, like .otherblock .myblock, $this of .myblock will return a full value, including the parent, so the whole concept won’t work properly.
Smarter breakpoints
Let’s say you store layout values on a map. You also have to change the layout on a particular responsive breakpoint so you define two layout maps: $small-layout and $big-layout. But, how do you fetch those values if you have one function (described on "Use maps or lists" section) that returns nested values from the default map (probably from the $big-layout)? Create another function for fetching the $small-layout? Add a layout map as an additional parameter for an existing function?
There’s a better option: create a smarter breakpoint mixin. This pattern comes from Susy, a framework for layouts and grids. Since Susy uses maps for layouts, on “susy-breakpoint” mixin you can set layout map as an optional parameter. Nothing magic happens, but when you access other Susy functions (like gutter()) inside the breakpoint mixin, it automatically returns the value based on your given layout map. The “smarter” breakpoint works the same, but with our custom function for fetching the map. It can even be a wrapper for susy-breakpoint if you use Susy (to use the same layout for both).
$small-layout: (
header: (
height: 30px
)
);
$big-layout: (
header: (
height: 60px
)
);
$default-layout: $big-layout;
$current-layout: $default-layout !default;
@mixin smart-breakpoint($breakpoint, $layout) {
$_temp: $current-layout;
$current-layout: $layout !global;
@media screen and ($breakpoint) {
@content;
}
$current-layout: $_temp !global;
}
.header {
height: layout-value(header, height); // the value will be 60px
@include smart-breakpoint('max-width: 768px', $small-layout) {
height: layout-value(header, height); // the value will be 30px
}
}
You can find the full (working) implementation here.
You can play around with the code to get how it works, but basically the mixin gets "layout" as a parameter and overrides the global layout value by adding !global flag at the beginning. When you use your map function inside the breakpoint mixin, the newly set value becomes the default value and you get the desired result. Before finishing the job, mixin resets the default value, so outside of a breakpoint, the map function returns to default results again.
Loop repetitive stuff
Sass provides @for and @each methods for looping repetitive stuff. But, where should you use them?
Let’s say you have to create 10 modifiers for a block. All of them do the same thing: change the icon or other property. Writing all these modifiers manually wouldn’t be the smartest decision; however, you can combine those previously mentioned patterns. With local variables and maps you can achieve a very clean and easily maintainable code.
In the following example, icons are stored as a map (class name as key and icon as value):
.icon {
$_icons: (
delete: icn-delete,
edit: icn-edit,
add: icn-add
);
&::before {
content: '';
}
@each $label, $icon in $_icons {
&--#{$label} {
&::before {
@include svg-sprite($icon);
}
}
}
}
Second-level color variables
Probably the biggest benefit of global Sass variables is the option to define colors and use them anywhere, without repetition of the HEX value. However, there are a lot of different naming strategies for colors, and it's very difficult to pick the best one.
We use the $color-[color]-[number] pattern (for example, $color-green-1) as a first-level name. This pattern is easy to use, as colors always start with a "color-" prefix, especially, if you use suggestions on your IDE. You can even group them by shades (from light to dark), such as on Material UI. However, using those variables can still be confusing, because they don't have any semantic meaning. To resolve this, you can use second-level naming, which is related to a function or object—for example, $color-text-primary, $color-action, etc. Those variables should relate to the first-level color variables to keep the possibility of using both, because some blue item isn't always an action.
$color-black: #000;
$color-white: #fff;
$color-gray: #bababa;
$color-text-primary: $color-black;
$color-text-secondary: $color-gray;
$background-color-default: $color-white;
However, be careful with this second-level naming pattern. It's very easy to overuse. Define only the most needed colors. If you have more than 10 second-level variables, it will be difficult to read. Moreover, you probably won't remember all those different variables, so it doesn't solve anything, anyway. You will still have to look at your variables list every time you need to use the color.
A Sass conclusion
Structuring your code with components helps you to write efficient code and maintain it easily. Other patterns reduce repetition, improve readability or just help you to do some things easier. However, the key point is to keep everything simple and consistent. Don’t overuse those patterns (or any other cool features of Sass) just because it looks cool—use them to solve real-world problems.