by Aleksandr Beliaev, Senior Frontend Developer at Nortal, August 28, 2019
The end result is an improved user experience for both users of design systems (designers, developers, analysts) and end users of a product. You can get a good overview of the advantages of using a design system here.
Regardless of which design system we deal with, the fonts, colors, icons and independent UI modules can be presented as structured data. The whole wealth of patterns covered by a system can be described then in terms of JSON –like data types: simple key-value pairs, objects or arrays. After all, implementations of some design systems already keep their CSS variables as JSON.
The aptly named Sass — Syntactically awesome style sheets — has all of that, too. Its variables can store any data, arrays are lists, and objects are Sass maps. Mixins can be used for any cases where you want not just to store data but to describe styling of a block that appears on an actual page. In this blog post, we discuss how can we use the rich variety of Sass tooling to make your codebase more representative of a design system it supports, leaner and more scalable.
A common way to organize a design system is around abstract design patterns (known as well as design tokens, atoms, or foundation) and component patterns (also familiar to many as components, blocks, modules). Abstract patterns can’t be put to use in markup right away; they need to be embodied in component patterns first. The latter are independent blocks of reusable styling that can be applied to a DOM tree according to certain rules (perfectly explained in the documentation for BEM front-end methodology).
Text styles are an example of an abstract pattern in a design system.
A peculiar thing about abstract patterns is that in practice they usually represent several basic design system properties clustered together. It’s likely you find not a single font size, color, or white space independently popping up here and there in the UI, but a batch of typographic properties (font weight, size, line height, etc.) going together. Line height and font size, for example, depend a lot on each other, and it makes little sense to use them separately. Same with color palettes, spacing or sizing patterns. Therefore, contrary to how Atomic design puts it, in a design system it may not make much sense to talk about “atoms” because those end up being grouped into molecules in a real product anyway.
Let’s see how different kinds of patterns in a design system can be represented with Sass tools.
What should omnipresent things such as colors, typography, or icons look like in code? We usually think of creating simple CSS/Sass variables, one value per each variable. See this example from Buzzfeed’s Solid design system:
$text-1: 1.75rem !default; // 28px
$text-2: 1.375rem !default; // 22px
$text-3: 1.125rem !default; // 18px
$text-4: 1rem !default; // 16px
$text-5: .875rem !default; // 14px
$text-6: .75rem !default; // 12px
This solution contributes to reusability and consistency to a certain degree. Storing data like this may present some issues though:
:focus
, :hover
) or @media
breakpoints. Or see, for example, how icons are organized in the style guide for Atlassian products, with many distinctive parameters serving as groups;There might be better tools to use. Sass maps solve most of those problems. They can be nested, can be created or changed dynamically in JavaScript fashion, and it’s perfectly possible to fill them with as much nested data as needed.
One bonus feature is better code readability. Design systems feature a lot of hierarchical relationships. For instance, a font size may be a part of a typographic scale, text style group, or UI component. And it’s much easier to convey relationships between parents and children in a map than it is to manage a huge Bootstrap-like list of variables. Thus, maps are more error-proof — if it’s easier to see the relationship, you or any other developer are less likely to give a wrong name or place to a new value.
Let’s see how some frequent abstract patterns of a design system could be represented in Sass.
Colors are stored as variables in the style guides for most design systems. From many examples we can see that developers need to communicate some sort of structure to colors. With single-value variables the only way to do it is to utilize huge chainable names, such as bpk-link-white-hover-color
, --global--secondary-title--FontSize
or $color-accent-success-lightness-400
(an example from a project of my own!). Even if there is a common scheme for naming, the rationale behind it often gets lost during the many stages of development.
A better approach is to utilise a map. You can define all the colors by the proper names as originally suggested here, like this:
$colors-by-name: (
'limeade': #46a400,
'olivetone': #6d800d,
'limed-ash': #a1bc80,
'hippie-green': #508457,
// ...
);
Then we add a second map that communicates color usage. This map can be structured by any number of criteria to your liking. Obviously, those criteria should come from the design system you follow. The criteria could include so-called “semantic” grouping (warning, danger…), a theme (light, dark…), properties of color, even components you are building:
$colors-by-function: (
'buttons': (
'primary-button': (
'background-color': map-get($colors-by-name, limeade),
'border-color': map-get($colors-by-name, olivetone),
'hover': (
'background-color': map-get($colors-by-name, limed-ash),
'border-color': map-get($colors-by-name, hippie-green),
)
),
'secondary-button': (
// ...
)
)
);
What is important here is that the color definitions referring to UI or function should be separated from colors themselves. By detaching design entities from their UI application, we can allow them to be independently applied anywhere. For example, the same color that you use to denote a “success” state could be reused as an accent or a call-to-action somewhere else.
Button appearance is a good example of how many variations a design pattern can have.
You can write a custom mixin such as this one written to accommodate your way to structure colors and apply them. This trick could help to represent and use your color patterns in a much DRYer way.
The same principles apply to typography. Many design systems define text styles as ready-to-serve combinations of typographic properties. Instead of using tens of standalone variables for font sizes, weights, etc., we could create a map of all the typographic styles (grouped by font family).
$typographic-styles: (
'body-font': ( // eg. Roboto
'banner-heading': (
'font-size': 20px,
'line-height': 1.6,
'font-weight': bold,
'color': #ffffff,
),
'card-heading': (
'font-size': 15px,
'line-height': 1.3,
'font-weight': bold,
'color': #5c696d,
),
'paragraph-text': (
'font-size': 15px,
'line-height': 1.4,
'color': #252525,
)
)
);
With a simple mixin, you can apply a text style to any component, thus making your UI much more consistent and easier to use.
Sass lists and maps can be useful when defining spacing-related design patterns. It’s often that we use same spacing across different components (for example, a card and a slide of a carousel could have same padding or gutter). We also most likely would want it to change consistently on various screen sizes. To achieve this we could define spacing in a map and then use a mixin to apply this styling to specific components:
$spacing-styles: (
'page-spacing': (
'padding-x': 40px,
'padding-y': 30px 40px,
'large': (
'padding-x': 80px
)
),
'card-spacing': (
'padding': 16px 20px,
'medium': (
'padding': 20px 28px
)
)
);
Again, this allows us to increase reusability of this abstract pattern while keeping it as complex as necessary.
In the end, with abstract patterns presented as maps we get better design consistency and saner codebase. With a few mixins you can apply the whole pattern in any part of the UI and then focus on more unique features.
Note that it’s important to maintain pattern names future-proof by having them general enough. This way developers would feel free to apply them to any component. A rule of the thumb would be to at least avoid giving same names to your abstract patterns as to your components.
What about components themselves?
Component patterns is what actually gets applied to your markup. They require a slightly different approach.
It’s not uncommon to style some parts of UI in the same way as other parts, but with minor changes. Not unlike developers from the world of object oriented programming, we need a way to create components that can inherit design already defined in another component. It would greatly help to reduce code duplication and improve design consistency.
This is achievable if we present our components as Sass mixins that can be applied to a different set of CSS classes. For that we can have base CSS class as a parameter:
@mixin c-tabs($base_class: &) {
@at-root {
{$base_class}--direction-horizontal {
...
}
{$base_class}__inner {
...
}
{$base_class}__tab {
...
&--state-highlighted {
}
}
}
}
With this setup, c-tabs
could easily become c-navigation
or c-theme-switcher
, and we would be able to amend the inherited design in each case. You can read more about this technique here.
From technical side, it helps us to decouple markup from design. If we can reuse a pattern under different aliases, it’s easier to drop complex naming conventions that don’t make sense for non-CSS people (such as Bootstrap classes, media class from OOCSS, or a million of utility classes from the likes of Tailwind).
This also allows us to mirror design process. It’s pretty often that designers just repurpose an existing pattern in order to cut down on amount of work and to provide consistency. With component mixins we basically follow designer workflow, discriminating repeatable patterns and unique features.
Both maps and mixins might serve as perfect tools for building a front-end equivalent of a design system — even if the design system is not there yet. And since CSS reflects the current state of the UI, this “grounds-up” design system might end up being more accurate than design system documentation. Hopefully, with this tooling and the principles we have discussed you’ll be able to keep your codebase and design system more consistent with less effort. Thanks for reading!