Design tokens manage and store User Interface (UI) decisions such as spacing, color, typography, and motion. These decisions are stored in a config-like format to support the needs of any platform (Web, mobile OS, etc). In this blog post, I'll demonstrate how you can leverage design tokens and SCSS mixins to capture a system's design language. The final output will be a collection of SCSS mixins that map to design tokens and can help to reel in messy CSS inconsistencies amongst dev teams and close the gap between designers and developers with great success.
Background: The visual language of your product
A product's visual design, at it's core, is defined by basic visual elements.
- Colour
- Typograph
- Space and Motion
All components and layouts of any digital design system fit into those categories. In a design system, this is referred to as the visual language. Unfortunately, many products define their visual language across a wide gap; from design mocks to CSS files, these elements are defined and redefined over again. This is often the root cause of design inconsistencies, tangled CSS files, and expensive iterations. Design tokens bridge the gap by moving a design language to its own layer where designers and developers can draw from and contribute iterations to. Essentially, it serves to dry up the visual language to a single location. It's a simple, yet powerful concept with a list of advantages:
- Controlled change - Designers can iterate with confidence knowing design decisions cascade across all digital teams from a single location.
- Strengthened design and developer communication - Designers and developers manage the design layer together, digitally and conceptually.
- Speeds up UI development - Developers draw from a limited pool of UI decisions which takes out the guess work.
- Eliminates visual bugs - Teams of developers pull digital design decisions from the same shared resource that are applied directly to their UI code. This cuts away common inconsistency bugs in CSS that occur on small and large teams.
- Increased design consistency - Teams of designers share managed design decisions.
The following touches on these principles and builds up a basic working example.
Design Tokens
Design Tokens, named by Jina Anne, were first created by Salesforce and appear in their design system. The code snippet below demonstrates a scale of spacing tokens as JSON format.
spaceTokens: {
"xxxs": "4",
"xxs": "8",
"xs": "12",
"s": "16",
"m": "24",
"l": "32",
"xl": "48",
"xxl": "72",
"xxxl": "96",
"xxxxl": "120",
};
Moving these decisions to its own layer (like the tokens file above) in the production process creates harmony between design effort and development effort. UI inconsistencies are eliminated and changes to UI decisions cascade immediately.
Naming convention
A naming convention applied to the scale of each design property make values accessible to both designers and developers. This example uses a t-shirt size naming convention, incrementing the scale values between xxxl to xxxs.
A naming convention further fine tunes the relationship between the design and development effort. In the example below, a designer changes the button's margin from a token value of xl to m. The developer uses that same token value to apply the update.
SCSS Mixins
SCSS mixins allow you to group CSS declarations into reusable pieces and therefore are a good fit for delivering and accessing the values captured in design tokens.
// Use of mixins
.myButton {
@include background-color('red', 'x-light');
@include margin-left('xs');
@include padding-horizontal('m');
@include padding-vertical('xl');
}
// Generated output
.myButton {
background-color: #cc3656;
marign-left: 8px;
padding-left: 12px;
padding-right: 12px;
padding-top: 24px;
padding-bottom: 24px;
}
The concept of design tokens is not limited to SCSS, in fact, just the opposite. Design tokens can be platform agnostic (e.g., JSON file) which allow them to support any environment. You can follow the Design Tokens Working Group here.
Putting it all together
Below you can see how tokens, mixins and functions are combined together to write components that use the design system. Following that is a running sandbox example.
Tokens
tokens.scss
The tokens here are stored directly in a SCSS file.
$spaceMap: (
xxxs: 4px,
xxs: 8px,
xs: 12px,
s: 16px,
m: 24px,
l: 32px,
xl: 48px,
xxl: 72px,
xxxl: 96px,
xxxxl: 120px,
);
$textSizeMap: (
xxs: 8px,
...
xxl: 72px,
);
$textWeightsMap: (
x-light: 100,
light: 400,
bold: 700,
x-bold: 900,
);
$colorMap: (
red: (
base: #cc3656,
xx-light: #f597ac,
x-light: #e06a84,
light: #d84f6d,
dark: #9b344a,
x-dark: #702031,
xx-dark: #571120,
),
green: (
{...}
),
blue: (
{...}
),
gray: (
{...}
),
{...}
);
$tokens: (
color: $colorMap,
space: $spaceMap,
textSize: $textSizeMap,
textWeight: $textWeightsMap,
);
If this were a production product, the tokens could later be moved into a JSON file and the SCSS could be generated in a build process. Libraries like JSON-TO-SCSS could help.
Mixins
styles.scss
This file creates all the mixins to be consumed by the application. The mixins take care of accessing the design tokens.
@import './tokens.scss';
/********SPACE******/
@function getSize($size) {
$spaceTokens: map-get($tokens, 'space');
@return map-get($spaceTokens, $size);
}
/**padding**/
@mixin padding($size) {
padding: getSize($size);
}
@mixin padding-top($size) {
padding-top: getSize($size);
}
{...}
@mixin padding-vertical($size) {
padding-top: getSize($size);
padding-bottom: getSize($size);
}
/**margin**/
@mixin margin($size) {
margin: getSize($size);
}
{...}
/********COLOR******/
@function getColor($color, $size: 'base') {
$colorTokens: map-get($tokens, 'color');
$selected: map-get($colorTokens, $color);
@return map-get($selected, $size)
}
/**color**/
@mixin color($color, $size: 'base') {
color: getColor($color, $size);
}
/**background-color**/
@mixin background-color($color, $size: 'base') {
background-color: getColor($color, $size);
}
/*****TYPOGRAPHY****/
$textSizeTokens: map-get($tokens, 'textSize');
$textWeightTokens: map-get($tokens, 'textWeight');
@mixin font($size, $type: 'body') {
@if ($type == 'body') {
font-size: map-get($textSizeTokens, $size);
font-weight: map-get($textWeightTokens, 'light');
}
@if ($type == 'heading') {
font-size: map-get($textSizeTokens, $size);
font-weight: map-get($textWeightTokens, 'bold');
}
}
{...}
The decision behind the naming style for each mixin is based around a mental mapping to traditional CSS. For example, a CSS developer should already understand what padding-left means and therefore a mixin with the same naming is easily digestible.
// CSS
padding-left: 16px;
// Easily maps to
@include padding-left('m');
We chose this naming scheme because one of our team goals was:
"Support a large audience of varying expertise with limited training and high flexibility"
The CSS-like naming scheme made it approachable for users of varying experience levels to work with the design system. In another circumstance, you might decide that it makes more sense to reach for a terse naming convention, like Tachyons does:
@include pl('m'); //<-- padding-left: 'medium';
In Nathan Curtis's article, Space in Design Systems, he takes an approach that maps design tokens into spatial concepts. Offering limited flexibility to encourage greater consistency. In the article's spacing section, he abstracts groupings of CSS into single classes to choose from, each one being a spatial "concept". For example, squish-inset reduces space by 50% top and bottom which could generate this output:
.squish-inset {
padding-left: 10px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
}
SCSS Functions
Along with the mixins, there are SCSS functions (like getSize()) included to gain direct access to a design token's value. This way you can compose CSS property / values that aren't available as mixins, but still dip into the design system. Here, getColor() is used to reach into the color palette.
border: 2px solid getColor('green', 'light');
Component example
button.component.scss
The mixins are imported into a module's SCSS file and consumed in a CSS class.
@import '../styles/styles.scss';
.myButton {
@include padding-horizontal('m');
@include padding-vertical('s');
@include font('xl', 'heading');
@include background-color('green', 'x-light');
border: 2px solid getColor('green', 'light');
}
Sandbox example
Takeaways
For some projects, the barrier to entry of implementing a full-blown design system is fairly great. Design tokens, however, can be applied to your existing project without the sign-off of a design system. Design tokens offer great value in their own right and implantation effort can start relatively low.
Design tokens can also serve as a gateway to opening up the conversation about how a design system can help align process, design, development and governance around your product.