Behavioural Tokens and Developer Handoff
The principal of design tokens is something that has been extensively documented, with definitive articles guiding you on how to give them meaningful names. There’s even a spec!
However, as powerful as design tokens are, they can fall foul of Maslow’s Hammer, especially given the limitations of the tools used by designers to consume them.
This can lead to design-developer handover being something of a brute-force effort.
In this post I’ll explore one aspect of this that we’ve faced, how we’ve attempted to address it, and the impact it has on the designer-developer handover (and relationship).
Now read on…
Wait, what is a design token?
Just to set a baseline for my ramblings, and to make it easier for you, dear reader, to point out the many elementary mistakes I have made, please indulge me in a whistle-stop tour of design tokens.
Generic tokens
From a developers perspective, they may just appear to be a way of avoiding magic numbers. That is, perhaps, true for the first category of design tokens that one is likely to encounter; the [Generic]/[Foundation]/[Primitive] delete according to personal taste Token.
We may, for instance, define a suite of blue colors including1…
const genericTokens = {
...
dsColorPaletteBlue07: "#5d3fd3",
dsColorPaletteBlue08: "#7d65dc",
dsColorPaletteBlue09: "#9584d9",
...
};
Nice and simple - a pair of blue colours in our blue color scale; they have no association with any particular layer, state or semantic messaging.
Semantic tokens
On top of these, we may build Semantic Tokens, which imbue some element of intent as to how these tokens may be used.
For instance, we may decide that we want something like this…
const semanticTokens = {
...
dsColorPrimaryForegroundDefault: genericTokens.dsColorPaletteBlue07,
dsColorPrimaryForegroundHover: genericTokens.dsColorPaletteBlue08,
dsColorPrimaryForegroundActive: genericTokens.dsColorPaletteBlue09,
...
}
We have now defined in our primary color suite some tokens to describe what the foreground color should be at resting state, and what the foreground color should be when hovered, both using aliases to the generic tokens2.
Component tokens
Now we have our semantic tokens, we can finally connect them to the components with Component Tokens.
These may take the form…
const componentTokens = {
...
dsButtonBorderColorDefault: semanticTokens.dsColorPrimaryForegroundDefault,
dsButtonBorderColorHover: semanticTokens.dsColorPrimaryForegroundHover,
dsButtonBorderColorActive: semanticTokens.dsColorPrimaryForegroundActive,
...
}
and we can slot those into our button.
Composite tokens
Slightly off to the side of the earlier three, there are several categories of semantic tokens that may need to express more than just a single scalar value. These include
typography (font-size
, line-height
, font-weight
, &c.), border (width
, style
, color
, &c.), shadow (color
, blur
, spread
, &c.).
For these, the concept of a Composite token is introduced to allow people to express a single value for multiple properties.
However…
There is no such concept to allow for the expression of multiple values for a single property.
Enter the Behavioural token
First it is important to nail down what problem I believe the behavioural token to be solving.
The Behavioural Token exists to express the common set of values required to describe a property across multiple states.
Whilst spending six weeks staring at buttons, trying to ask “Right, but what is a button, when you get right down to it?”3, it became apparent that there were a number of components (or parts thereof) that shared an identical pattern of behaviours.
Interactive behaviours
For instance, an interactive control that used a semantic token of dsColorPrimaryForegroundDefault
for its border color
could easily have the colors for the hover, active and disabled states derived.
Combined with the need4 to map on the appropriate system color in forced colors mode, that makes eight colors that could be encoded in a single color.
As with composite tokens, they are not flat, and so might be expressed thusly…
const behaviouralTokens = {
...
dsInteractivePrimaryForegroundColor: {
default: semanticTokens.dsColorPrimaryForegroundDefault,
hover: semanticTokens.dsColorPrimaryForegroundHover,
active: semanticTokens.dsColorPrimaryForegroundActive,
disabled: semanticTokens.dsColorForegroundDisabled,
forcedDefault: 'ButtonBorder',
forcedHover: 'HighlightText',
forcedActive: 'HighlightText',
forcedDisabled: 'GrayText',
},
...
}
Judicious use of, say, a sass mixin can drastically reduce the implementation time and risk for developers5:
@use "sass:map";
@mixin applyBehavioural($prop, $token) {
#{$prop}: map.get($token, 'default');
&:hover {
#{$prop}: map.get($token, 'hover');
}
&:active {
#{$prop}: map.get($token, 'active');
}
&:disabled {
#{$prop}: map.get($token, 'disabled');
}
@media (forced-colors: active) {
#{$prop}: map.get($token, 'forcedDefault');
&:hover {
#{$prop}: map.get($token, 'forcedHover');
}
&:active {
#{$prop}: map.get($token, 'forcedActive');
}
&:disabled {
#{$prop}: map.get($token, 'forcedDisabled');
}
}
}
When composited with other behavioural tokens, this gives designers and developers an incredibly expressive and compact way of describing, say, an interactive control:
const compositeBehaviouralTokens = {
...
dsInteractivePrimaryColor: {
borderColor: behaviouralTokens.dsInteractivePrimaryForegroundColor,
color: behaviouralTokens.dsInteractivePrimaryForegroundColor,
backgroundColor: behaviouralTokens.dsInteractivePrimaryBackgroundColor,
},
...
}
“But wait,” I hear you cry. “Isn’t this just a box?”. Almost, but not entirely. Components like the Box (or whatever your favourite system calls them) tend not to respond to user interaction, or any other state. This is about creating reusable behavioural definitions at the pre-component level that may be composited with other properties in an un-opinionated way.
Responsive behaviours
This is possibly a more familiar concept, and one which is seen more regularly. Many tokens will need to respond to, for instance, viewport size. Typography would be the classic example of this:
const semanticTokens = {
...
dsTypographyLabelRegular: {
small: genericTokens.dsTypographyFontSize2,
medium: genericTokens.dsTypographyFontSize3,
...
},
...
}
But why does this matter?
The classic flow for implementation of a component might look something like this.
In Figma, a designer will have produced a few variations of the state of a button:
Default:
/--------------\
| {Label} | // border-color: componentTokens.dsButtonBorderColorDefault
\--------------/
Hover:
/--------------\
| {Label} | // border-color: componentTokens.dsButtonBorderColorHover
\--------------/
Active:
/--------------\
| {Label} | // border-color: componentTokens.dsButtonBorderColorActive
\--------------/
The chain of tokens for that third value is
componentTokens.dsButtonBorderColorActive
»semanticTokens.dsColorPrimaryForegroundActive
»genericTokens.dsColorPaletteBlue09
»#9584d9
Now this is a simple example that doesn’t even consider, say, theming, or forced colors. An incredibly naïve implementation might iterate through each color computing increasingly complex selectors.
A contrived example may look something like this:
// file: tokens.scss
$dsColorPaletteBlue09: #9584d9;
// file: button.scss
.button {
&.primary {
@media (prefers-color-scheme: light) {
border-color: $dsColorPaletteBlue09;
&:hover {
border-color: $dsColorPaletteBlue08;
}
&:active {
border-color: $dsColorPalletteBlue07;
}
}
@media (prefers-color-scheme: dark) {
border-color: $dsColorPaletteBlue01;
&:hover {
border-color: $dsColorPaletteBlue02;
}
&:active {
border-color: $dsColorPalletteBlue03;
}
}
@media (forced-colors: active) {
border-color: ButtonBorder;
&:hover {
border-color: HighlightText;
}
&:active {
border-color: HighlightText;
}
}
}
}
A worked example in Sass
Instead, let us start with a compact implementation written to minimise duplication (and thus risk of error), and see where we can meet the design token hierarchy.
// First, sass variables for our generic tokens
// file: tokens.scss
$dsColorPaletteBlue09: #9584d9;
$dsColorPaletteBlue03: #d4cdef;
// Second; define a theme - our semantic tokens
// file: theme.scss
:root {
@media (prefers-color-scheme: light) {
--ds-color-primary-foreground-default: $dsColorPaletteBlue07;
--ds-color-primary-foreground-hover: $dsColorPaletteBlue08;
--ds-color-primary-foreground-active: $dsColorPaletteBlue09;
}
@media (prefers-color-scheme: dark) {
--ds-color-primary-foreground-default: $dsColorPaletteBlue03;
--ds-color-primary-foreground-hover: $dsColorPaletteBlue02;
--ds-color-primary-foreground-active: $dsColorPaletteBlue01;
}
}
// Thirdly, our behavioural token
// file: behaviours.scss
$ds-interactive-primary-foreground-color: (
default: #{var(--ds-color-primary-foreground-default)},
hover: #{var(--ds-color-primary-foreground-hover)},
active: #{var(--ds-color-primary-foreground-active)},
);
// And then finally, we use it, with an optional component token
// file: button.scss
$ds-button-border-primary: $ds-interactive-primary-foreground-color;
.button {
&.primary {
@include applyBehavioural('border-color', $ds-button-border-primary);
@include applyBehavioural('color', $ds-button-foreground-primary);
}
&.secondary {
@include applyBehavioural('border-color', $ds-button-border-secondary);
@include applyBehavioural('color', $ds-button-foreground-secondary);
}
}
This is only one way we could do it; instead of having our behavioural token as Sass variables we could use css variables to even further reduce the amount of generated css (albeit at an increase in runtime complexity). You could also generate stylesheets in javascript if you don’t like writing Sass (and I wouldn’t blame you).
Hopefully though, it is clear, that the result is source code which much more concisely reflects the intent of the styles, and drastically reduces the chances of error, or need for generating code.
Now this approach should not come as a shock to anyone; many developers working on design systems will have come to similar conclusions and implemented helpers. The emphasis I wish to make is that we need to be better joined up, as designers and developers, at each level of design tokens - using the same definitions and logic to derive the final state of each component variant.
Conclusion
Thank you for getting this far6. As with many posts like this, I haven’t presented an actual solution. In our current greenfield design system we are at the very early stages of exploring how this works.
Ultimately we are starting from a point of:
- CSS and Sass are very powerful and dynamic tools
- Figma has only a limited subset of these capabilities
In our case now, we are experimenting with optimising our designs for their implementation - working out how best to express the designs given the full capabilities of the implementation - and then working backwards to see how we can map that onto existing workflows and tooling without overly disrupting current practice.
So far things are looking promising; Figma variables can be used much more powerfully than they often are, and with a little motivation from an awkward developer, we’re finding new ways of working towards the Devsign Utopia.
Remember:
We choose to implement a Button in this Design System and do the other things, not because it is easy, but because it is hard.
-
With apologies to Nathan if I haven’t got all the names quite right. ↩
-
As an aside, where possible, semantic tokens (and indeed any higher order token) should preserve aliasing for as long as possible in order to preserve the intent and origin behind every value. This is particularly relevant for computed tokens, but that’s another post… ↩
-
Still don’t know ↩
-
Yes, a need - another post to come ↩
-
Some advanced shenanigans are required to apply multiple of these in a way that minimises selector duplication ↩
-
Unless you just skipped to read the end ↩