When it comes to styling, you know what I really like? Utility classes! All the boring stuff about styling such as spacing, coloring, text formatting, etc. can be easily handled by using a utility class (e.g., m-sm
– margin small, color-primary
, font-lg
– font-size large). What surprised me is that I'm not the sole fan of this approach to CSS architecture. There are lots of devs out there preferring small and single-purpose classes than large semantic ones. This movement in front-end is named Atomic CSS, and it is gaining on velocity as we speak.
Including utility classes (also called helpers) in your project can be easily done by installing a CSS library such as Bootstrap, TailwindCSS or any other that provides utility classes out of the box. However, from my perspective, this approach is not recommended when you just want to use spacing helpers, because they usually come as a secondary feature. You will end up installing the whole package with all its components and features and use only a small fraction. This may produce a negative effect to your bundle size and app performance accordingly.
Since we are going to write our own utility classes, we now have the power to dictate the nomenclature. However, I wouldn't get too creative about these, because the name should be as short as possible, uniform and yet intuitive enough to be easily memorable.
TL;DR
Introducing the concept of pre-processing styles using Sass/Scss. If you are already familiar with the concept and you are just looking for a code snippet that generates utilities for spacing, skip to the last heading.
Spacing Utilities
In today's article, we are going to handle spacings only. Our Scss code (e.g., input.scss
) will be compiled into CSS code (output.css
) that will contain a bunch of similar-looking classes like ones in the example below.
/* output.css */
.m-8 {
margin: 8px !important;
}
@media (min-width: 480px) {
.mt-sm-16 {
margin-top: 16px !important;
}
}
@media (min-width: 768px) {
.mx-md-32 {
margin-left: 32px !important;
margin-right: 32px !important;
}
}
The advantage of Sass pre-processor is in its unique syntax and flexibility achieved with features such as variables, nested rules, loops, mixins, etc. Once written, code goes through the compiler and generates CSS code as output.
Making It Work
To get there, we're going to take a walk through the whole process. I am going to use vanilla Javascript and Vite as a bundler. You can either clone my repo or follow the steps:
- Create Vite project and pick vanilla Javascript boilerplate:
npm create vite@latest
- Install Sass:
npm i sass -D
Now let's create two partial files and name them _config.scss
and _spacing.scss
. In config file we're going to declare a map with key-value pairs that will be consumed in spacing file to generate utility classes like those in the output.css
file.
// _config.scss
$spacing: (
0: 0px,
8: 8px,
16: 16px,
32: 32px,
64: 64px
);
Then, the logic comes into play. By looping through $properties
and $spacing
maps, the preprocessor generates classes containing margin
or padding
property with values in pixels.
// _spacing.scss
@use 'config' as *;
$properties: (
'm': margin,
'p': padding
);
@each $prefix, $property in $properties {
@each $suffix, $space in $spacing {
.#{$prefix}-#{$suffix} {
#{$property}: #{$space} !important;
}
}
}
Okay, we've got that one covered. This piece of code will generate classes for all-direction margins and paddings.
.m-8 {
margin: 8px !important;
}
Next we are going to do is to cover cases for each axis and direction.
// _spacing.scss
@use 'config' as *;
$properties: (
'm': margin,
'p': padding
);
$directions: (
't': top,
'b': bottom,
'l': left,
'r': right
);
$axes: 'y', 'x';
// Mapping $spacing values per $property
@each $prefix, $property in $properties {
@each $suffix, $space in $spacing {
.#{$prefix}-#{$suffix} {
#{$property}: #{$space} !important;
}
}
}
// Mapping $spacing values per $property and $axis
@each $prefix, $property in $properties {
@each $axis in $axes {
@each $suffix, $space in $spacing {
.#{$prefix}#{$axis}-#{$suffix} {
@if $axis == 'y' {
#{$property}-top: #{$space} !important;
#{$property}-bottom: #{$space} !important;
} @else if $axis == 'x' {
#{$property}-left: #{$space} !important;
#{$property}-right: #{$space} !important;
}
}
}
}
}
// Mapping $spacing values per $property and $direction
@each $prefix, $property in $properties {
@each $infix, $direction in $directions {
@each $suffix, $space in $spacing {
.#{$prefix}#{$infix}-#{$suffix} {
#{$property}-#{$direction}: #{$space} !important;
}
}
}
}
And those will look as the following.
.mx-8 {
margin: 8px !important;
}
.mt-8 {
margin: 8px !important;
}
Now we just need to analyze our code and look for potential improvements.
Minor Refactor
You may recognize a repetitive pattern in our nested loops. We are looping through $properties
and $spacing
three times, that is for each orientation individual and per axis, which can be simplified by nesting them together and loop through each of them one time only for all cases.
@each $prefix, $property in $properties {
@each $suffix, $space in $spacing {
// Cover all cases here
}
}
The first set that produces all-direction spacing (m-8
or p-8
) is generated without additional loops. But direction and axis-oriented spacings each need one additional loop. The resulting Scss will give us what we initially wanted – utility classes for spacing.
@each $prefix, $property in $properties {
@each $suffix, $space in $spacing {
.#{$prefix}-#{$suffix} {
#{$property}: #{$space} !important;
}
@each $axis in $axes {
.#{$prefix}#{$axis}-#{$suffix} {
@if $axis == 'y' {
#{$property}-top: #{$space} !important;
#{$property}-bottom: #{$space} !important;
} @else if $axis == 'x' {
#{$property}-left: #{$space} !important;
#{$property}-right: #{$space} !important;
}
}
}
@each $infix, $direction in $directions {
.#{$prefix}#{$infix}-#{$suffix} {
#{$property}-#{$direction}: #{$space} !important;
}
}
}
}
The job is almost done. To complete it, all we need to do is to add a validator to prevent developer from assigning values to $spacing
that either margin
or padding
property doesn't accept, such as auto, because padding: auto;
is not valid.
For this purpose, we're going to create _mixins.scss
and inside that file declare validate-unit
which will be used in _spacing.scss
.
// _mixins.scss
@mixin validate-unit($value, $value-type, $units...) {
@if type-of($value) != $value-type or index($units, unit($value)) == null {
@error "Invalid unit #{unit($value)} for value #{$value}.";
}
}
Inject the validator in the $spacing
loop.
// _spacing.scss
@use 'config' as *;
@use 'mixins' as *;
$properties: (
'm': margin,
'p': padding
);
$directions: (
't': top,
'b': bottom,
'l': left,
'r': right
);
$axes: 'y', 'x';
@each $prefix, $property in $properties {
@each $suffix, $space in $spacing {
@include validate-unit($space, number, px);
.#{$prefix}-#{$suffix} {
#{$property}: #{$space} !important;
}
@each $infix, $direction in $directions {
.#{$prefix}#{$infix}-#{$suffix} {
#{$property}-#{$direction}: #{$space} !important;
}
}
@each $axis in $axes {
.#{$prefix}#{$axis}-#{$suffix} {
@if $axis == 'y' {
#{$property}-top: #{$space} !important;
#{$property}-bottom: #{$space} !important;
} @else if $axis == 'x' {
#{$property}-left: #{$space} !important;
#{$property}-right: #{$space} !important;
}
}
}
}
}
Alright, we have a fully functioning utility class generator with a unit-check guard. The $spacing
map now only receives numerical values in px
, em
or rem
. We can use these classes to set spacings without "polluting" custom CSS files with margin or padding properties.
Our job here is done. (long pause) NOT! 😜
Making It Responsive
Almost every single website or app today is responsive. That means we cannot use our spacings as they are, because mobile spacings are usually small and growing as screen size increases. That's why we need to upgrade them to accept breakpoints too, so that we can dynamically change spacing based on the screen size.
<div class="m-8 m-sm-16 m-md-32 m-lg-64">Hi mom!</div>
Adding Breakpoints
First, $breakpoints
are configurable, hence they'll go to _config.scss
.
// _config.scss
$spacing: (...);
$breakpoints: (
'xs': 375px,
'sm': 480px,
'md': 768px,
'lg': 1080px,
'xl': 1440px,
'xxl': 1920px
);
Second, they have to be included in the (grand)parent loop because of the cascade rule (mobile-first design). We are dictating the generation of utility classes to increase min-width
from top to bottom and we want them to be grouped together by breakpoint.
End Result
Fully functioning utility class generator.
// _spacing.scss
@use 'config' as *;
@use 'mixins' as *;
$properties: (
'm': margin,
'p': padding
);
$directions: (
't': top,
'b': bottom,
'l': left,
'r': right
);
$axes: 'y', 'x';
// Classes without breakpoint abbreviation (e.g. m-16)
@each $prefix, $property in $properties {
@each $suffix, $space in $spacing {
@include validate-unit($space, number, px);
.#{$prefix}-#{$suffix} {
#{$property}: #{$space} !important;
}
@each $infix, $direction in $directions {
.#{$prefix}#{$infix}-#{$suffix} {
#{$property}-#{$direction}: #{$space} !important;
}
}
@each $axis in $axes {
.#{$prefix}#{$axis}-#{$suffix} {
@if $axis == 'y' {
#{$property}-top: #{$space} !important;
#{$property}-bottom: #{$space} !important;
} @else if $axis == 'x' {
#{$property}-left: #{$space} !important;
#{$property}-right: #{$space} !important;
}
}
}
}
}
// Classes WITH breakpoint abbreviation (e.g. m-sm-16)
@each $breakpoint, $breakpoint-value in $breakpoints {
@media (min-width: $breakpoint-value) {
@each $prefix, $property in $properties {
@each $suffix, $space in $spacing {
.#{$prefix}-#{$breakpoint}-#{$suffix} {
#{$property}: #{$space} !important;
}
@each $axis in $axes {
.#{$prefix}#{$axis}-#{$breakpoint}-#{$suffix} {
@if $axis == 'y' {
#{$property}-top: #{$space} !important;
#{$property}-bottom: #{$space} !important;
} @else if $axis == 'x' {
#{$property}-left: #{$space} !important;
#{$property}-right: #{$space} !important;
}
}
}
@each $infix, $direction in $directions {
.#{$prefix}#{$infix}-#{$breakpoint}-#{$suffix} {
#{$property}-#{$direction}: #{$space} !important;
}
}
}
}
}
}
Instead of writing about 2000 lines of repetitive CSS that is subjected to human error, we have achieved the same thing in under 100 lines of SCSS. Of course, this code can be additionally refactored to fit in fewer lines. But for the sake of readability, I'm going to leave it as is.
Potential Performance Issue
The hustle we went through really pays off! (a bit shorter pause) Or does it?! 🧐
What if hypothetically we end up using only a few of these classes in our project? For example, the team has decided to handle spacing by adding only bottom margin and x-axis padding to elements.
Don't worry, there's a solution for that too. Cleaning up unused CSS with a post-processor plugin will be covered in the Part 2.
Check out the repo