Whether developing a front end for Drupal or Nuxt, or any other web application, I've refined a standard structure for Sass projects that can be reused, extended, and customized without too many overrides or competing implementation approaches. I also rely a few key mixins and other tools to ensure that the styles can be used both during preprocessing (Sass variables) and post-processing (CSS custom properties).
The standard structure
You can find all kinds of recommended "architectures", but I generally like to keep to a pretty simple folder structure and number of files. The complexity of the project, however, may increase if we're dealing with a CMS like Drupal that likes to stuff a ton of its own extra markup and class names into the DOM. But assuming we have some control over the HTML—either because we're defining it ourselves or are comfortable using whatever tools/mechanisms are available to us to overwrite the standard markup—I find that more complexity = spending too much time figuring out where a particular style declaration should go that makes the most logical sense (when it really doesn't matter). Also, I prefer to use the .scss
syntax for Sass, because it looks more like css (brackets rather than just indents), but all of this is translatable between either (.scss
or .sass
).
Here's what my projects tend to start with:
sass/
abstracts/
_functions.scss
_generators.scss
_mixins.scss
_variables.scss
elements/
_base.scss
_buttons.scss
_forms.scss
_misc.scss
...
structure/
_header.scss
_main.scss
_menu.scss
_scaffold.scss
...
components/
about.scss
hero.scss
shared.scss
...
_init.scss
print.scss
all.scss
The abstracts/ directory and _init.scss
The abstracts/
directory contains Sass resources that we forward for use in our other .scss files by including the _init.scss
file first. So, for example, a file like about.scss
, which would contain styles specific to an "about" page/component, will include at its very top the following line:
@use "../init" as *;
Then, the remaining code in that file can access the functions, generators, mixins, and variables in the abstracts/
directory because the _init.scss
code looks like this:
@forward "abstracts/variables";
@forward "abstracts/functions";
@forward "abstracts/mixins";
@forward "abstracts/generators";
Variables
Variables need to be separate so that they can be used individually in the other "abstracts" files, but you could combine the other three into a single file if you want. I'd recommend including only Sass variables/maps in the _variables.scss
file, and using a "generator" (see below) to convert variables into CSS custom properties where and how desired. You'll want to define a set of key variables/maps such as $colors
, $breakpoints
, and $font-families
. For example:
$colors: (
"white": #ffffff,
"gray": #a9a9a9,
"black": #000000,
"text": #1f1f1f,
"navy": #012039,
"orange": #ff9f1c,
"red": #e71d36,
"blue": #2e9bc4,
"pink": #d43778,
);
$breakpoints: (
"xsmall": 320px,
"small": 640px,
"medium": 1024px,
"large": 1200px,
"xlarge": 1400px,
"xxlarge": 1920px,
);
$font-families: (
"display": '"Helvetica", "Arial", sans-serif',
"serif": '"Times New Roman", sans-serif',
"sans-serif": '"Helvetica", "Arial", sans-serif',
"monospace": '"Consolas", "Courier New", monospace',
);
When combined with the generate-fonts
function defined in the _generators.scss
file, though , we can add more complex combinations/variables. So we can define another map in our _variables.scss
file called $fonts
, place copies of the necessary font files at the file
locations defined and with the various extentions needed (eot
, svg
, ttf
, woff
, woff2
), so we can then use them in our original $font-families
map:
...
$fonts: (
"Roboto Mono Regular": (
"family": "Roboto Mono",
"weight": "400",
"style": "normal",
"file": "/assets/fonts/roboto-mono/roboto-mono-regular",
),
"Roboto Mono Bold": (
"family": "Roboto Mono",
"weight": "700",
"style": "normal",
"file": "/assets/fonts/roboto-mono/roboto-mono-bold",
),
);
...
$font-families: (
...
"monospace": '"Roboto Mono Bold", "Consolas", "Courier New", monospace',
);
Generators
The generators are technically mixins, but we only use them once in the all.scss
file in the Sass project root (see further detail/elaboration below). We call the generators before any other processing so that the results can be used/referenced in other parts of the project. I like to use three key generators (two of which have already been mentioned):
- Convert Sass variables/maps to CSS custom properties (mentioned earlier).
- Create variations on the original
$colors
variable/map for use in Sass, CSS, and in markup (i.e., as classes). - Produce the
@font-face
rules for custom font importing (mentioned earlier).
The resulting file looks like this (note the inclusion of some specific Sass modules at the top):
@use "sass:color";
@use "sass:math";
@use "sass:meta";
@use "variables";
@mixin generate-custom-properties($variables-map, $prefix, $key: "") {
@each $name, $value in $variables-map {
$key-copy: $key;
$key: #{$key}-#{$name};
@if meta.type-of($value) == "map" {
@include generate-custom-properties($value, $prefix, $key);
} @else {
:root {
--#{$prefix}#{$key}: #{$value};
}
}
$key: $key-copy;
}
}
@mixin generate-colors($colors-map) {
@each $name, $color in $colors-map {
:root {
$color-lightest: scale-color($color, $lightness: 92%);
$color-lighter: scale-color($color, $lightness: 65%);
$color-light: scale-color($color, $lightness: 45%);
$color-lightish: scale-color($color, $lightness: 25%);
$color-darkish: scale-color($color, $lightness: -15%);
$color-dark: scale-color($color, $lightness: -30%);
$color-darker: scale-color($color, $lightness: -50%);
$color-darkest: scale-color($color, $lightness: -75%);
--color-#{$name}: #{$color};
--color-#{$name}-lightest: #{$color-lightest};
--color-#{$name}-lighter: #{$color-lighter};
--color-#{$name}-light: #{$color-light};
--color-#{$name}-lightish: #{$color-lightish};
--color-#{$name}-darkish: #{$color-darkish};
--color-#{$name}-dark: #{$color-dark};
--color-#{$name}-darker: #{$color-darker};
--color-#{$name}-darkest: #{$color-darkest};
}
.text-color--#{$name} {
color: var(--color-#{$name});
}
.background-color--#{$name} {
background-color: var(--color-#{$name});
}
.hover-text-color--#{$name} {
&:hover {
color: var(--color-#{$name}) !important;
}
}
.hover-background-color--#{$name} {
&:hover {
background-color: var(--color-#{$name}) !important;
}
}
}
}
@mixin generate-fonts($fonts-map) {
@each $name, $value in $fonts-map {
@font-face {
font-family: '#{map-get($value, "family")}';
src: url('#{map-get($value, "file")}.eot');
src:
url('#{map-get($value, "file")}.eot?#iefix') format("embedded-opentype"),
url('#{map-get($value, "file")}.woff') format("woff"),
url('#{map-get($value, "file")}.ttf') format("truetype"),
url('#{map-get($value, "file")}.svg?#webfont') format("svg");
font-weight: #{map-get($value, "weight")};
font-style: #{map-get($value, "style")};
}
}
}
Mixins and functions
I like to define a number of mixins/functions that either I've written/refined or that I've found in other projects. There are some really great sources out there, too. And because these are just modular functions you don't need to install/integrate a complete library (despite what some of them would have you believe!): https://github.com/thoughtbot/bourbon, https://github.com/Famolus/awesome-sass, https://gerillass.com/. For breakpoints, though, I keep to just three and never deviate or change them:
@use "variables" as *;
@mixin above($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
$breakpoint-value: map-get($breakpoints, $breakpoint);
@media (min-width: $breakpoint-value) {
@content;
}
} @else {
@warn 'Invalid breakpoint: #{$breakpoint}.';
}
}
@mixin below($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
$breakpoint-value: map-get($breakpoints, $breakpoint);
@media (max-width: ($breakpoint-value - 1)) {
@content;
}
} @else {
@warn 'Invalid breakpoint: #{$breakpoint}.';
}
}
@mixin between($lower, $upper) {
@if map-has-key($breakpoints, $lower) and map-has-key($breakpoints, $upper) {
$lower-breakpoint: map-get($breakpoints, $lower);
$upper-breakpoint: map-get($breakpoints, $upper);
@media (min-width: $lower-breakpoint) and (max-width: ($upper-breakpoint - 1)) {
@content;
}
} @else {
@if (map-has-key($breakpoints, $lower) == false) {
@warn 'Invalid breakpoint (lower): #{$lower}.';
}
@if (map-has-key($breakpoints, $upper) == false) {
@warn 'Invalid breakpoint (upper): #{$upper}.';
}
}
}
Base styles
To keep things simple, I just include a single _base.scss
partial in the elements/
direcrory where I define some of the basic html/body, typography stuff (other systems can be much more elaborate). Remember that in these and all other Sass files (partials or otherwise) we need to include our _init.scss
resources:
@use "../init" as *;
html,
body {
line-height: var(--line-height-base);
font-style: normal;
font-size: var(--font-size-base-px);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-base);
color: var(--color-text);
}
p {
line-height: var(--line-height-base);
font-size: var(--font-size-base);
font-family: var(--font-family-sans-serif);
margin: var(--margin-p);
color: var(--color-text);
&:last-of-type {
margin-bottom: 0;
}
}
...
Other partials and standalone Sass files
Elements
In that same folder (elements/
) I then try to break out closely-related interface parts into additional partials files that are somewhat intuitive so that maintenance is easier and we don't end up with an unwieldy number of files. As shown in the original structure, these might include a _buttons.scss
file:
@use "../init" as *;
button,
a.button {
color: var(--color-white);
font-size: var(--font-size-base);
font-weight: var(--font-weight-strong);
background-color: var(--color-pink);
padding: 1.35rem 1.75rem;
&:hover,
&:active,
&:focus {
color: var(--color-white);
background-color: var(--color-pink-lightish);
}
&.skip-to-final-code {
align-items: center;
display: flex;
&:before {
content: "";
background: url("/icons/code-icon.svg") no-repeat center/contain;
width: 1.55rem;
height: 1.55rem;
display: inline-block;
}
}
}
Structure
And, finally, the last set of partials that we include on every page are those in the structure/
directory. These tend to be macro-level / high-up-the-DOM-tree styles that help to position layouts, provide grids and other sizing classes, layout menus, and handle overall responsiveness for columns, images, etc. For example, the _columns.scss
partial might include the following:
@use "../init" as *;
:root {
$column-gap: 0.75rem;
}
.columns {
display: flex;
flex-direction: column-reverse;
margin-top: (-$column-gap);
&:last-child {
margin-bottom: (-$column-gap);
}
&:not(:last-child) {
margin-bottom: calc(1.5rem - #{$column-gap});
}
@include above("small") {
flex-direction: row;
}
&--centered {
justify-content: center;
}
&--gapless {
margin-top: 0;
& > .column {
margin: 0;
padding: 0 !important;
}
&:not(:last-child) {
margin-bottom: 1.5rem;
}
&:last-child {
margin-bottom: 0;
}
}
&--mobile {
display: flex;
}
&--multiline {
flex-wrap: wrap;
}
&--vcentered {
align-items: center;
}
}
Components
The "components" directory, I reserve for standalone Sass files (i.e., not 'partials') that we optionally include on a particular page to keep the size of the CSS assets low. I tend to use "components" to mean pages or larger modular components—and in Drupal, specifically, these will tie into specific Twig templates that handle the markup for paragraphs entities... more to come on that in a separate post (@TODO).
Pulling it "all" together
As I alluded to earlier, we then combine the generators with the partials importers in an all.scss
file in the root of the Sass project:
@use "init" as *;
@include generate-colors($colors);
@include generate-fonts($fonts);
@include generate-custom-properties($breakpoints, "breakpoint");
@include generate-custom-properties($font-families, "font-family");
@import "elements/*";
@import "structure/*";
With these pieces in place, we can extend the basic implementation to include, say, additional column classes that respond to different breakpoints. First we add a Sass variable/map, then run it through the custom properties generator, and finally append the needed style declarations to the existing columns file:
...
$column-widths: (
"full": 100%,
"three-quarters": 75%,
"two-thirds": 66.6667%,
"half": 50%,
"one-third": 33.3333%,
"one-quarter": 25%,
"one-fifth": 20%,
"two-fifths": 40%,
"three-fifths": 60%,
"four-fifths": 80%,
);
...
...
@include generate-custom-properties($column-widths, "column-width");
...
...
@include above("medium") {
.columns--2col-25-75 {
& > .column:nth-of-type(1) {
width: var(--column-width-one-quarter);
}
& > .column:nth-of-type(2) {
width: var(--column-width-three-quarters);
}
}
.columns--2col-33-67 {
& > .column:nth-of-type(1) {
width: var(--column-width-one-third);
}
& > .column:nth-of-type(2) {
width: var(--column-width-two-thirds);
}
}
...
}
...