Sass Techniques from the Trenches
Publikováno: 8.1.2019
Having been in the web development industry for more than 14 years, I’ve seen and written my fair share of good and bad CSS. When I began at Ramsey Solutions five years ago, I was introduced to Sass. It blew my mind how useful it was! I dove right in and wanted to learn everything I could about it. Over the past five years, I’ve utilized a number of different Sass techniques and patterns and fell in love with some … Read article
The post Sass Techniques from the Trenches appeared first on CSS-Tricks.
Having been in the web development industry for more than 14 years, I’ve seen and written my fair share of good and bad CSS. When I began at Ramsey Solutions five years ago, I was introduced to Sass. It blew my mind how useful it was! I dove right in and wanted to learn everything I could about it. Over the past five years, I’ve utilized a number of different Sass techniques and patterns and fell in love with some that, to steal Apple’s phrase, just work.
In this article, I’ll explore a wide range of topics:
- The power of the ampersand
- Variables and scoping
- The importance of imports
- Mixin' it up
- Getting functional
- The selector order that placeholders mess up
In my experience, finding the balance between simple and complex is the crucial component to making great software. Software should not only be easy for people to use, but for you and other developers to maintain in the future. I’d consider these techniques to be advanced, but not necessarily clever or complex, on purpose!
"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?"
—The Elements of Programming and Style (2nd Edition), Chapter 2
With that in mind, let’s first look at Sass’ ampersand.
The power of the ampersand
There are many different naming conventions you can use to organize your CSS. The one I enjoy using the most is SUIT, a variation of BEM (which is short for Block, Element, Modifier). If you’re unfamiliar with SUIT or BEM, I’d recommend taking a peek at one or both of them before moving on. I’ll be using the SUIT convention throughout the rest of this article.
Whatever naming convention you choose, the base idea is that every styled element gets its own class name, prepended with the component name. This idea is important for how some of the following organization works. Also, this article is descriptive, not prescriptive. Every project is different. You need to do what works best for your project and your team.
The ampersand is the main reason I like to use SUIT, BEM, and conventions like them. It allows me to use nesting and scoping without either biting back with specificity. Here’s an example. Without using the ampersand, I would need to create separate selectors to create -title
and -content
elements.
.MyComponent {
.MyComponent-title {}
}
.MyComponent-content {}
// Compiles to
.MyComponent .MyComponent-title {} // Not what we want. Unnecessary specificity!
.MyComponent-content {} // Desired result
When using SUIT, I want the second result for -content
to be how I write all my selectors. To do so, I would need to repeat the name of the component throughout. This increases my chance to mistype the name of the component as I write new styles. It’s also very noisy as it ends up ignoring the beginning of many selectors which can lead to glossing over obvious errors.
.MyComponent {}
.MyComponent-title {}
.MyComponent-content {}
.MyComponent-author {}
// Etc.
If this were normal CSS, we’d be stuck writing the above. Since we’re using Sass, there’s a much better approach using the ampersand. The ampersand is amazing because it contains a reference to the current selector along with any parents.
.A {
// & = '.A'
.B {
// & = '.A .B'
.C {
// & = '.A .B .C'
}
}
}
You can see in the above example how the ampersand references each selector in the chain as it goes deeper into the nested code. By utilizing this feature, we can create new selectors without having to rewrite the name of the component each and every time.
.MyComponent {
&-title {}
&-content {}
}
// Compiles to
.MyComponent {}
.MyComponent-title {}
.MyComponent-content {}
This is great because we can take advantage of the ampersand to write the name of the component one time and simply reference the component name throughout. This decreases the chance that the component name is mistyped. Plus, the document as a whole becomes easier to read without .MyComponent
repeated all over the code.
There are times when the component needs a variant or modifier, as they’re called in SUIT and BEM. Using the ampersand pattern makes it easier to create modifiers.
<div class="MyComponent MyComponent--xmasTheme"></div>
.MyComponent {
&--xmasTheme {}
}
// Compiles to
.MyComponent {}
.MyComponent--xmasTheme {}
"But, what about modifying the child elements?" you might ask. "How are those selectors created? The modifier isn’t needed on every element, right?"
This is where variables can help!
Variables and scoping
In the past, I’ve created modifiers a few different ways. Most of the time, I’d rewrite the special theme name I want to apply when modifying the element.
.MyComponent {
&-title {
.MyComponent--xmasTheme & {
}
}
&-content {
.MyComponent--xmasTheme & {
}
}
}
// Compiles to
.MyComponent-title {}
.MyComponent--xmasTheme .MyComponent-title {}
.MyComponent-content {}
.MyComponent--xmasTheme .MyComponent-content {}
This gets the job done, but I’m back to rewriting the component name in multiple places, not to mention the modifier name. There’s definitely a better way to do this. Enter Sass variables.
Before we explore Sass variables with selectors, we need to understand how they’re scoped. Sass variables have scope, just like they would in JavaScript, Ruby, or any other programming language. If declared outside of a selector, the variable is available to every selector in the document after its declaration.
$fontSize: 1.4rem;
.a { font-size: $fontSize; }
.b { font-size: $fontSize; }
Variables declared inside a selector are scoped only to that selector and its children.
$fontSize: 1.4rem;
.MyComponent {
$fontWeight: 600;
font-size: $fontSize;
&-title {
font-weight: $fontWeight; // Works!
}
}
.MyComponent2 {
font-size: $fontSize;
&-title {
font-weight: $fontWeight; // produces an "undefined variable" error
}
}
We know variables can store font names, integers, colors, etc. Did you know it can also store selectors? Using string interpolation, we can create new selectors with the variable.
// Sass string interpolation syntax is #{VARIABLE}
$block: ".MyComponent";
#{$block} {
&-title {
#{$block}--xmasTheme & {
}
}
}
// Compiles to
.MyComponent {}
.MyComponent-title {}
.MyComponent--xmasTheme .MyComponent-title {}
That’s cool, but the variable is globally scoped. We can fix that by creating the $block
variable inside the component declaration, which would scope it to that component. Then we can re-use the $block
variable in other components. This helps DRY up the theme modifier.
.MyComponent {
$block: '.MyComponent';
&-title {
#{$block}--xmasTheme & {
}
}
&-content {
#{$block}--xmasTheme & {
}
}
}
// Compiles to
.MyComponent {}
.MyComponent-title {}
.MyComponent--xmasTheme .MyComponent-title {}
.MyComponent-content {}
.MyComponent--xmasTheme .MyComponent-content {}
This is closer, but again, we have to write the theme name over and over. Let’s store that in a variable too!
.MyComponent {
$block: '.MyComponent';
$xmasTheme: '.MyComponent--xmasTheme';
&-title {
#{$xmasTheme} & {
}
}
}
This is much better! However, we can improve this even further. Variables can also store the value of the ampersand!
.MyComponent {
$block: &;
$xmasTheme: #{&}--xmasTheme;
&-title {
#{$xmasTheme} & {
}
}
}
// Still compiles to
.MyComponent {}
.MyComponent-title {}
.MyComponent--xmasTheme .MyComponent-title {}
Now that’s what I’m talking about! "Caching" the selector with ampersand allows us to create our modifiers at the top and keep the theme modifications with the element it’s modifying.
"Sure, that works at the top level," you say. "But what if you are nested really deep, like eight levels in?" You ask great questions.
No matter how deep the nest, this pattern always works because the main component name is never attached to any of the children, thanks to the SUIT naming convention and ampersand combo.
.MyComponent {
$block: &;
$xmasTheme: #{&}--xmasTheme;
&-content {
font-size: 1.5rem;
color: blue;
ul {
li {
strong {
span {
&::before {
background-color: blue;
#{$xmasTheme} & {
background-color: red;
}
}
}
}
}
}
}
}
// Compiles to
.MyComponent-content {
font-size: 1.5rem;
color: blue;
}
.MyComponent-content ul li strong span::before {
background-color: blue;
}
/*
* The theme is still appended to the beginning of the selector!
* Now, we never need to write deeply nested Sass that's hard to maintain and
* extremely brittle: https://css-tricks.com/sass-selector-combining/
*/
.MyComponent--xmasTheme .MyComponent-content ul li strong span::before {
background-color: red;
}
Code organization is the main reason I like to use this pattern.
- It’s relatively DRY
- It supports the "opt-in" approach, which keeps modifiers with the elements they modify
- Naming stuff is hard but this enables us to reuse common element names like "title" and "content"
- It’s low-lift to add a modifier to a component by placing the modifier class on the parent component
"Hhhmmmmm... doesn’t that get hard to read though after you create a bunch of different components? How do you know where you’re at when everything is named &-title
and &-content
?"
You continue to ask great questions. Who said the source Sass had to be in one file? We can import those components, so let’s turn to that topic!
The importance of imports
One of Sass’ best features is @import
. We can create separate Sass files (partials) and import them into other Sass files that compile together with the imported file located at the spot it’s imported. This makes it easy to package up related styles for components, utilities, etc. and pull them into a single file. Without @import
, we’d need to link to separate CSS files (creating numerous network requests, which is badong) or write everything in a single stylesheet (which is tough to navigate and maintain).
.Component1 {
&-title {}
&-content {}
&-author {}
}
.Component2 {
&-title {}
&-content {}
&-author {}
}
.Component3 {
&-title {}
&-content {}
&-author {}
}
.Component4 {
&-title {}
&-content {}
&-author {}
}
.Component5 {
&-title {}
&-content {}
&-author {}
}
// A couple hundred lines later...
.Component7384 {
&-title {}
&-content {}
&-author {}
}
// WHERE AM I?
One of the more popular methodologies for organizing Sass files is the 7-1 Pattern. That’s seven distinct folders containing Sass files that are imported into a single Sass file.
Those folders are:
- abstracts
- base
- components
- layout
- pages
- themes
- vendor
Use @import
to pull each Sass file in those folder into a main Sass file. We want to import them in the following order to maintain good scope and avoid conflicts during compilation:
- abstracts
- vendor
- base
- layout
- components
- pages
- themes
@import 'abstracts/variables';
@import 'abstracts/functions';
@import 'abstracts/mixins';
@import 'vendors/some-third-party-component';
@import 'base/normalize';
@import 'layout/navigation';
@import 'layout/header';
@import 'layout/footer';
@import 'layout/sidebar';
@import 'layout/forms';
@import 'components/buttons';
@import 'components/hero';
@import 'components/pull-quote';
@import 'pages/home';
@import 'pages/contact';
@import 'themes/default';
@import 'themes/admin';
You may or may not want to use all of these folders (I personally don’t use the theme folder since I keep themes with their components), but the idea of separating all of styles into distinct files makes it easier to maintain and find code.
More of the benefits of using this approach:
- Small components are easier to read and understand
- Debugging becomes simpler
- It’s clearer to determine when a new component should be created — like when a single component file gets to be too long, or the selector chain is too complex
- This emphasizes re-usage — for example, it might make sense to generalize three component files that essentially do the same thing into one component
Speaking of re-usage, there are eventually patterns that get used often. That’s when we can reach for mixins.
Mixin’ it up
Mixins are a great way to reuse styles throughout a project. Let’s walk through creating a simple mixin and then give it a little bit of intelligence.
The designer I work with on a regular basis always sets font-size
, font-weight
, and line-height
to specific values. I found myself typing all three out every time I needed to adjust the fonts for a component or element, so I created a mixin to quickly set those values. It’s like a little function I can use to define those properties without having to write them in full.
@mixin text($size, $lineHeight, $weight) {
font-size: $size;
line-height: $lineHeight;
font-weight: $weight;
}
At this point, the mixin is pretty simple—it resembles something like a function in JavaScript. There’s the name of the mixin (text
) and it takes in three arguments. Each argument is tied to a CSS property. When the mixin is called, Sass will copy the properties and the pass in the argument values.
.MyComponent {
@include text(18px, 27px, 500);
}
// Compiles to
.MyComponent {
font-size: 18px;
line-height: 27px;
font-weight: 500;
}
While it’s a good demonstration, this particular mixin is a little limited. It assumes we always want to use the font-size
, line-height
, and font-weight
properties when it’s called. So let’s use Sass’ if
statement to help control the output.
@mixin text($size, $lineHeight, $weight) {
// If the $size argument is not empty, then output the argument
@if $size != null {
font-size: $size;
}
// If the $lineHeight argument is not empty, then output the argument
@if $lineHeight != null {
line-height: $lineHeight;
}
// If the $weight argument is not empty, then output the argument
@if $weight != null {
font-weight: $weight;
}
}
.MyComponent {
@include text(12px, null, 300);
}
// Compiles to
.MyComponent {
font-size: 12px;
font-weight: 300;
}
That’s better, but not quite there. If I try to use the mixin without using null
as a parameter on the values I don’t want to use or provide, Sass will generate an error:
.MyComponent {
@include text(12px, null); // left off $weight
}
// Compiles to an error:
// "Mixin text is missing argument $weight."
To get around this, we can add default values to the parameters, allowing us to leave them off the function call. All optional parameters have to be declared after any required parameters.
// We define `null` as the default value for each argument
@mixin text($size: null, $lineHeight: null, $weight: null) {
@if $size != null {
font-size: $size;
}
@if $lineHeight != null {
line-height: $lineHeight;
}
@if $weight != null {
font-weight: $weight;
}
}
.MyComponent {
&-title {
@include text(16px, 19px, 600);
}
&-author {
@include text($weight: 800, $size: 12px);
}
}
// Compiles to
.MyComponent-title {
font-size: 16px;
line-height: 19px;
font-weight: 600;
}
.MyComponent-author {
font-size: 12px;
font-weight: 800;
}
Not only do default argument values make the mixin easier to use, but we also gain the ability to name parameters and give them values that may be commonly used. On Line 21 above, the mixin is being called with the arguments out of order, but since the values are being called out as well, the mixin knows how to apply them.
There’s a particular mixin that I use on a daily basis: min-width
. I prefer to create all my sites mobile first, or basically with the smallest viewport in mind. As the viewport grows wider, I define breakpoints to adjust the layout and the code for it. This is where I reach for the min-width
mixin.
// Let's name this "min-width" and take a single argument we can
// use to define the viewport width in a media query.
@mixin min-width($threshold) {
// We're calling another function (scut-rem) to convert pixels to rem units.
// We'll cover that in the next section.
@media screen and (min-width: scut-rem($threshold)) {
@content;
}
}
.MyComponent {
display: block;
// Call the min-width mixin and pass 768 as the argument.
// min-width passes 768 and scut-rem converts the unit.
@include min-width(768) {
display: flex;
}
}
// Compiles to
.MyComponent {
display: block;
}
@media screen and (min-width: 48rem) {
.MyComponent {
display: flex;
}
}
There are a couple of new ideas here. The mixin has a nested function called @content
. So, in the .MyComponent
class, we’re no longer calling the mixin alone, but also a block of code that gets output inside the media query that’s generated. The resulting code will compile where @content
is called. This allows the mixin to take care of the @media
declaration and still accept custom code for that particular breakpoint.
I also am including the mixin within the .MyComponent
declaration. Some people advocate keeping all responsive calls in a separate stylesheet to reduce the amount of times @media
is written out in a stylesheet. Personally, I prefer to keep all variations and changes that a component can go through with that component’s declaration. It tends to make it easier to keep track of what’s going on and help debug the component if something doesn’t go right, rather than sifting through multiple files.
Did you notice the scut-rem
function in there? That is a Sass function taken from a Sass library called Scut, created by David The Clark. Let’s take a look at how that works.
Getting functional
A function differs from a mixin in that mixins are meant to output common groups of properties, while a function modifies properties based on arguments that return a new result. In this case, scut-rem
takes a pixel value and converts it to a rem value. This allows us to think in pixels, while working with rem units behind the scenes to avoid all that math.
I’ve simplified scut-rem
in this example because it has a few extra features that utilize loops and lists, which are out of the scope of what we’re covering here. Let’s look at the function in its entirety, then break it down step-by-step.
// Simplified from the original source
$scut-rem-base: 16 !default;
@function scut-strip-unit ($num) {
@return $num / ($num * 0 + 1);
}
@function scut-rem ($pixels) {
@return scut-strip-unit($pixels) / $scut-rem-base * 1rem;
}
.MyComponent {
font-size: scut-rem(18px);
}
// Compiles to
.MyComponent {
font-size: 1.125rem;
}
The first thing to note is the declaration on Line 2. It’s using !default
when declaring a variable, which tells Sass to set the value to 16 unless this variable is already defined. So if a variable is declared earlier in the stylesheet with a different value, it won’t be overridden here.
$fontSize: 16px;
$fontSize: 12px !default;
.MyComponent {
font-size: $fontSize;
}
// Compiles to
.MyComponent {
font-size: 16px;
}
The next piece of the puzzle is scut-strip-unit
. This function takes a px, rem, percent or other suffixed value and removes the unit label. Calling scut-strip-unit(12px)
returns 12 instead of 12px. How does that work? In Sass, a unit divided by another unit of the same type will strip the unit and return the digit.
12px / 1px = 12
Now that we know that, let’s look at the scut-strip-unit
function again.
@function scut-strip-unit ($num) {
@return $num / ($num * 0 + 1);
}
The function takes in a unit and divides it by 1 of the same unit. So if we pass in 12px, the function would look like: @return 12px / (12px * 0 + 1)
. Following the order of operations, Sass evaluates what’s in the parentheses first. Sass smartly ignores the px
label, evaluates the expression, and tacks px
back on once it’s done: 12 * 0 + 1 = 1px
. The equation is now 12px / 1px
which we know returns 12.
Why is this important to scut-rem
? Looks look at it again.
$scut-rem-base: 16 !default;
@function scut-rem ($pixels) {
@return scut-strip-unit($pixels) / $scut-rem-base * 1rem;
}
.MyComponent {
font-size: scut-rem(18px);
}
On Line 4, the scut-strip-unit
function removes px
from the argument and returns 18
. The base variable is equal to 16
which turns the equation into: 18 / 16 * 1rem
. Remember, Sass ignores any unit until the end of the equation, so 18 / 16 = 1.125
. That result multiplied by 1rem
gives us 1.125rem
. Since Scut strips the unit off of the argument, we can call scut-rem
with unit-less values, like scut-rem(18)
.
I don’t write that many functions because I try to keep the stuff I create as simple as possible. Being able to do some complex conversions using something like scut-rem
is helpful though.
The selector order that placeholders mess up
I really don’t like to use placeholders and @extend
in my code. I find it easy to get in trouble with them for a couple different reasons.
Be careful what is extended
I tried writing out some examples to demonstrate why using @extend
can be problematic, but I have used them so little that I can’t create any decent examples. When I first learned Sass, I was surrounded by teammates who’ve already gone through the trials and tribulations. My friend Jon Bebee wrote an extremely excellent article on how @extend
can get you into trouble. It’s a quick read and worth the time, so I’ll wait.
About those placeholders...
Jon proposes using placeholders as a solution to the problem he outlines: Placeholders don’t output any code until they’re used with @extend
.
// % denotes an extended block
%item {
display: block;
width: 50%;
margin: 0 auto;
}
.MyComponent {
@extend %item;
color: blue;
}
// Compiles to
.MyComponent {
display: block;
width: 50%;
margin: 0 auto;
}
.MyComponent {
color: blue;
}
OK, wait. So it output .MyComponent
twice? Why didn’t it simply combine the selectors?
These are the questions I had when I first started using placeholders (and then subsequently stopped). The clue is the name itself. Placeholders simply hold a reference to the place in the stylesheet they were declared. While a mixin copies the properties to the location it is used, placeholders copy the selector to the place where the placeholder was defined. As a result, it copies the .MyComponent
selector and places it where %item
is declared. Consider the following example:
%flexy {
display: flex;
}
.A {
color: blue;
}
.B {
@extend: %flexy;
color: green;
}
.C {
@extend: %flexy;
color: red;
}
// Compiles to
.B, .C {
display: flex;
}
.A {
color: blue;
}
.B {
color: green;
}
.C {
color: red;
}
Even though B and C are declared further down in the stylesheet, the placeholder places the extended properties tall the way up to where it was originally declared. That’s not a big deal in this example because it’s really close to the source where it’s used. However, if we’re adhering to something like the 7-1 Pattern we covered earlier, then placeholders would be defined in a partial in the abstracts folder, which is one of the first imported files. That puts a lot of style between where the extend is intended and where it’s actually used. That can be hard to maintain as well as hard to debug.
Sass Guidelines (of course) does a great job covering placeholders and extend and I would recommend reading it. It not only explains the extend feature, but at the end, advocates against using it:
Opinions seem to be extremely divided regarding the benefits and problems from
@extend
to the point where many developers including myself have been advocating against it, [...]
There are many other features of Sass I didn’t cover here, like loops and lists, but I’ve honestly haven’t relied on those features as much as the ones we did cover in this article. Take a look through the Sass documentation, if for nothing else, to see what things do. You may not find a use for everything right away, but a situation may come up and having that knowledge in your back pocket is priceless.
Let me know if I missed something or got something wrong! I’m always open to new ideas and would love to discuss it with you!
Further Reading
- Sass Style Guide - Thoughts and recommendations for keeping Sass clean and maintainable.
- What a CSS Code Review Might Look Like - Predominantly looks at CSS, but includes tips to code review Sass files.
- Defining and Dealing with Technical Debt - We talked a lot about DRY methods and efficiency in this article. This post talks about the various consequences of verbose or inefficient code.
- Loops in CSS Preprocessors - We didn't cover loops here, but Miriam Suzanne goes in depth in this article.
- Sass Snippets - A collection of handy Sass mixins and other Sass patterns.
The post Sass Techniques from the Trenches appeared first on CSS-Tricks.