A common need with a lot of projects is the ability to define groups of styles together and then applying them later on.
Take Google's Polymer project for example.
--paper-font-display1: {
@apply --paper-font-common-base;
font-size: 34px;
font-weight: 400;
letter-spacing: -.01em;
line-height: 40px;
}
This can then be applied to any class, or even a utility class which can be reused.
While using the concept of design tokens I've noticed that it would be very useful to store groups of styles like as design tokens (or design mixins if you wish).
In my mind, there would be several parts needed to make this work.
My background is in web development so I know the last two steps would be relatively easy because there are a lot of pre-processors available. I'm not that familiar with iOS and Android.
What are your thoughts? I'm interested in anyone's thoughts on what the pros and cons of this might be.
I think this is definitely something that would be useful. One specific example is creating reusable text styles that define a number of properties (font-size, line-height, color, font-weight, etc.). This does fit in with defining component-level tokens, where you could think of these groups as just components.
Because Style Dictionary focuses on the token level, and you could structure your tokens and groups of tokens in any way, you could achieve something similar to what you outline without any extra transforms. I think the only thing that is missing is if you want to extend groups of tokens inside Style Dictionary itself.
{
"component": {
"font": {
"base": {
"font-size": { "value": "{size.font.base.value}" },
"font-weight": { "value": "400" },
"color": { "value": "{color.font.base.value}" }
}
}
}
}
Then in the config/format you can output a CSS class or SCSS mixin with those properties in it. I don't know of the top of my head how Android / iOS would handle it, but we could figure something out. I need to work on adding a component tokens example in our examples, #246, maybe that might help.
I think I understand what you are saying. I've tried to have a go but I'm failing on a couple of areas. Mainly I'm not sure how to loop across each different component in order to create an output for each group of properties.
As a test, I tried to get an output of:
--component-button: {
font-size: 12px;
color: red;
padding: 20px;
border-width: 0;
text-align: center;
}
I have this so far:
"component": {
"button": {
"font-size": { "value": "12px" },
"color": { "value": "red" },
"padding": { "value": "20px" },
"border-width": { "value": "0"},
"text-align": { "value": "center" }
}
}
StyleDictionary.registerFormat({
name: 'css/components',
formatter: function(dictionary, config) {
return _.map(dictionary.allProperties, function(prop) {
if (prop.path[0] === 'component') {
return '--' + prop.path[0] + "-" + prop.path[1] + ': {\n ' + prop.attributes.item + ': ' + prop.value + ';' + ' }';
}
})
}
})
At the moment it's looping through each property and therefore outputting for each property instead of each component.
Which results in this:
,,,,,,,,--component-button {
font-size: 12px; },--component-button {
color: red; },--component-button {
padding: 20px; },--component-button {
border-width: 0; },--component-button {
text-align: center; },,,,
I'll keep playing around, but if you see an easy way to solve my problem above, let me know.
Many thanks.
Try this:
StyleDictionary.registerFormat({
name: 'css/components',
formatter: function(dictionary, config) {
return Object.keys(dictionary.properties.component).map(function (key) {
return `--component-${key}: {` +
Object.keys(dictionary.properties.component[key]).map(function (key2) {
let prop = dictionary.properties.component[key][key2];
return `${key2}: ${prop.value}`;
}).join('\n') +
'\n}'
})
}
});
Note: I did not actually test this code, but it should probably work. This solution is a bit fragile as it expects everything under the namespace 'component' to have the same structure, as in all children are the component/group and the children of that are the props with their keys as CSS property names. There are probably more robust solutions to account for other structures like nested groups or state/modifiers of groups.
Hi @dbanksdesign. Thanks, that worked right out the box. Nice one.
I can play around with some ideas for how it might be more robust. I'll let you know how I get on.
So I managed to tweak it a bit more just to get it working with my project, it's not really more robust yet but I think I can make it better once I get the basics working.
StyleDictionary.registerFormat({
name: 'css/components',
formatter: function(dictionary, config) {
return Object.keys(dictionary.properties).map(function (key3) {
// Outputs component properties
if (key3 === 'component') {
return Object.keys(dictionary.properties.component).map(function (key) {
return `--component-${key}: {\n` +
Object.keys(dictionary.properties.component[key]).map(function (key2) {
let prop = dictionary.properties.component[key][key2];
return `\t${key2}: ${prop.value};`;
}).join('\n') +
'\n}\n'
}).join('\n');
// Outputs token properties
} else {
return variablesWithPrefix(' --', dictionary.allProperties) + '\n';
}
}).join('\n');
}
});
The two things I've noticed
{size.font.base} aren't being computed. I think this is because we're using dictionary.properties. For the time being, is there an easy way to resolve these references?I realised that it's actually probably easier to do the following regarding the first point I mentioned.
source: ['properties/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'build/css/',
files: [{
destination: 'variables.css',
format: 'css/variables'
},{
destination: 'components.css',
format: 'css/components'
}]
}
}
The only downside is that I'll have to create a custom transform for all platforms to exclude the group styles from the output. Something worth thinking about if we wanted to make this more flexible.
For your point 2, dictionary.properties should be have references resolved and tokens transformed at this point. You should probably be referencing {size.font.base.value} to get it resolved properly. The reason is the resolution is "dumb" in that it simply copies whatever it finds at that object path and resolutions happen after transforms take place.
For excluding the groups what you could do is create 2 style dictionaries in a node file:
const StyleDictionary = require('style-dictionary');
StyleDictionary.extend({
source: ['properties/**/*.json', 'components/**/*.json'],
// ...
}).buildAllPlatforms();
StyleDictionary.extend({
source: ['properties/**/*.json']
// ...
}).buildAllPlatforms();
Or you could define a filter to filter out the component group stuff and use it on the files you don't want it.
Doh 🤦♂️, my bad. I missed that I had to reference the value directly.
The filter option looks interesting regarding excluding the groups.
Thanks again for all your help. I'm very interested to see where this technique takes me.
I've been playing around a little with the concept of storing groups of properties and found some interesting results.
One interesting use case has been an idea I'm working on which makes it easier for managing colours for typography. One of the traditional issues with specifying colour has been that when you specify only one color property like, background colour you risk the text colour already on the element not being compatible, as well as any other elements within the containing block you apply your background colour to. One way I've been exploring to aid this has been to create a colour schema or theme that specifies what the colour should be different elements contained within the element the background colour is applied to.
For example:
%color_theme {
@extend %color_theme_light;
color: var(--color);
background-color: var(--background-color);
}
%color_theme_reverse {
@extend %color_theme_dark;
color: var(--color);
background-color: var(--background-color);
}
%color_theme_dark {
--heading-color: COLOR_PRIMARY;
--paragraph-color: COLOR_WHITE;
--link-color: COLOR_PRIMARY;
--legal-color: COLOR_DARK_GREY;
--background-color: COLOR_BLACK;
--color: COLOR_WHITE;
}
%color_theme_light {
--heading-color: COLOR_BLACK;
--paragraph-color: COLOR_BLACK;
--link-color: COLOR_PRIMARY;
--legal-color: COLOR_DARK_GREY;
--background-color: COLOR_WHITE;
--color: COLOR_BLACK;
}
In another file, I have all text elements like h1-6, p, a etc to inherit the appropriate custom property. This allows me to easily reconfigure the colour theme for my website and also any sections of my site where the background colour changes.
Currently, I'm using a PostCSS plugin which allows me to apply these properties in groups. Here you can see that being able to extend another group of properties would be very valuable as it allows you to easily change the colour theme of an entire site easily from the configuration files.
What would it take to be able to extend groups of tokens inside Style Dictionary?
This example also represents a few other opportunities/challenges as well. For example, from a design system point of view, an author doesn't care much that one property might be a CSS custom property, or a SCSS/CSS variable, however for this particular implementation is matters for the technique to work. I wonder how this could be defined as tokens, yet still, work across platforms with their different technical implementations.
Extending groups, as far as I'm aware, is outside of the functionality of Style Dictionary. But it is pretty flexible, I'm sure you could do it with some custom transforms and formats.
You could do something in a slightly different way that makes less use of CSS syntax (scoped variable syntax to control theming), but achieve the same effect. You could follow something similar to @didoo's setup described in the mutli-brand multi-platform example also his article goes in depth on his setup. You could think of your themes, 'color_theme_light' and 'color_theme_dark' as 'brands'. What you would do then is for each theme, create a new style dictionary that uses all your common tokens + your theme tokens to create outputs for each theme.
I made a really quick and simple example: https://github.com/dbanksdesign/sd-theme-example
Granted, this way you would be pushing the functionality of scoped variables into style dictionary rather than in CSS, but that also might make it easier to translate to other languages? I do want to see how we can push Style Dictionary to output code that takes advantage of the languages a bit more like using actual variable references in code rather than resolving the references in Style Dictionary. I hope this helps.
Thanks for going to the effort to provide an example and explaining another technique. I'm sorry I couldn't reply sooner, life got pretty hectic and I haven't had much chance to concentrate on my coding projects.
I'm just re-looking at this and unfortunately, I don't think the examples you showed help cater for my use case because I was using themes in a more granular context within one site itself, not multiple brands/sites.
I decided to simplify my problem by not concentrating on getting Style Dictionary to extend groups of tokens within itself however I was trying to find a better way to translate groups of tokens than manually specifying them within the format function.
Do you have any suggestions? My first attempt was to create a filter which identifies which properties should be classified as groups of properties but then I hit a brick wall trying to create a format for these because I couldn't identify all the properties which should form part of the "group".
This is the filter I created for example:
StyleDictionary.registerFilter({
name: "group",
matcher: function(prop) {
var path = prop.path.join("-");
_.forEach(["font-style", "color-theme"], function(name) {
if (path.startsWith(name)) {
prop.attributes.category = "group";
}
});
return prop.attributes.category === "group";
}
});
example json
{
"font": {
"style" : {
"body": {
"font-family" : { "value": "Arial"},
"letter-spacing" : { "value": "-0.03em"},
"line-height" : { "value": "1.4"}
}
}
}
}
I was trying to output them to look something like:
.font-style-body {
font-family: Arial;
letter-spacing: -0.03em;
line-height: 1.4;
}
Below is the format I gave up on. I'm not sure how to concatenate the group name and then loop through the properties for the group while allowing for the possibility that some groups of properties might have different levels of nesting so I always want to loop through the last level for the properties.
StyleDictionary.registerFormat({
name: "css/utilityClass",
formatter: function(dictionary, config) {
return _.map(dictionary.allProperties, function(prop) {
var group = prop.path;
var groupName = group.join("-");
var pathName = prop.path.join("-");
console.log(groupName);
var to_ret_prop =
"." +
groupName +
" {\n" +
"\t" +
prop.attributes.subitem +
": " +
prop.value +
";" +
"\n}";
return to_ret_prop;
})
.filter(function(strVal) {
return !!strVal;
})
.join("\n");
}
});
This is probably not the best way to go about it but it's the best idea that came to mind. I think ideally the json config could be structured so Style Dictionary could tell which were groups of properties and which were individual, but I haven't found a way to realise that in Style Dictionary yet.
Anyway this is mainly just a brain dump on my progress with this.
Ok I think I got something pretty good:
const StyleDictionary = require('style-dictionary');
const groups = ["font-style", "color-theme"];
StyleDictionary.registerTransform({
name: "attribute/group",
type: 'attribute',
matcher: function(prop) {
return groups.includes(prop.path.slice(0,2).join('-'));
},
transformer: function(prop) {
return {
// Brittle logic, the group is the 3rd level of the object path
group: true,
groupName: prop.path.slice(0,3).join('-')
}
}
});
StyleDictionary.registerFormat({
name: 'css/group',
formatter: function(dictionary, config) {
const groupMap = dictionary.allProperties.reduce(function(prev,curr) {
if (!prev[curr.attributes.groupName]) {
prev[curr.attributes.groupName] = [];
}
prev[curr.attributes.groupName].push(curr);
return prev;
},{});
return Object.keys(groupMap).map(function(key) {
let props = groupMap[key];
return `.${key} {
${props.map(function(prop) {
return ` ${prop.name}: ${prop.value};`
}).join('\n')}
}`
}).join('\n\n');
}
});
StyleDictionary.extend({
properties: {
"font": {
"style" : {
"body": {
"font-family" : { "value": "Arial"},
"letter-spacing" : { "value": "-0.03em"},
"line-height" : { "value": "1.4"}
}
}
}
},
platforms: {
css: {
buildPath: 'build/',
transforms: ['attribute/group'],
files: [{
destination: 'groups.css',
format: 'css/group'
}]
}
}
}).buildAllPlatforms();
Given the tokens and the output you want, that should work. You can tweak the code so it better fits more of your use-case, but I think it's a good start.
Thank you again. This has helped me out a considerable amount.
I had the thought to move the logic of whether a token is a group or not by adding a token name like the following:
{
"font": {
"style" : {
"body": {
"font-family" : { "value": "Arial"},
"letter-spacing" : { "value": "-0.03em"},
"line-height" : { "value": "1.4"},
"type": "group"
}
}
}
}
However, with a transform I don't think I'm able to traverse the whole or original structure, or am I? If I could, I could maybe sweep through it once to find the position of the type: group property and then sweep through it again to let Style Dictionary know which properties are part of a group and what their respective group names are.
You aren't able to traverse the whole original structure in a transform, but you can do it in a format. So you could write a recursive function that walks the object, like style dictionary does when it is looking for tokens, and when it comes across an object with an attribute key of "type" and value of "group" you could do something with the tokens of that level. I've actually played with something similar when defining component tokens, in the object structure I added either "child": true or "modifier": true so that I could know in the format how to construct the class name. Here is the token JSON:
{
"component": {
"picker": {
"background-color": { "value": "{color.background.base.value}" },
"border-width": { "value": "{size.border.width.base.value}" },
"border-color": { "value": "{color.border.base.value}" },
"max-height": { "value": 0.5 },
"item": {
"child": true,
"color": { "value": "{color.font.base.value}" },
"font-size": { "value": "{size.font.base.value}" },
"padding": { "value": "{size.padding.base.value}" },
"active": {
"modifier": true,
"background-color": { "value": "{color.background.alt.value}" }
}
}
}
}
}
And the function I ran inside a formatter:
function componentSCSS(object, indent, path) {
var indent = indent || '';
var path = path || [];
var toRet = '';
for (const key in object) {
if (object.hasOwnProperty(key)) {
const element = object[key];
if (element.hasOwnProperty('value')) {
toRet += `\n${indent}${key}: ${element.value};`;
} else if (_.isPlainObject(element)) {
if (element.child) {
var className = path.concat(key).join('-');
toRet += `\n}\n.${className} {${componentSCSS(element, indent, path.concat(key))}\n${indent}`;
} else {
var newIndent = indent + ' ';
var modifier = '';
if (element.modifier) { modifier = '&' }
toRet += `\n${indent}${modifier}.${key} {${componentSCSS(element, newIndent, path.concat(key))}\n${indent}}`;
}
}
}
}
return toRet;
}
Very cool. I had a play around with the code provided on its own. I can see how you're constructing the string by checking if certain attributes are true. It's a bit too advanced for me to understand exactly how it's working but I'll see if I can apply the same concept to my project.
Hey @limitlessloop wanted to see if you needed any more help with this issue or if we can close it. We also have a discussion issue #268 about token organization and how to handle it properly moving forward. Thanks!
Yes, of course, thanks again.
Thanks @limitlessloop - please add any color / thinking / suggestions to #268 - it would help us move SD forward for everyone!
Most helpful comment
For your point 2,
dictionary.propertiesshould be have references resolved and tokens transformed at this point. You should probably be referencing{size.font.base.value}to get it resolved properly. The reason is the resolution is "dumb" in that it simply copies whatever it finds at that object path and resolutions happen after transforms take place.For excluding the groups what you could do is create 2 style dictionaries in a node file:
Or you could define a filter to filter out the component group stuff and use it on the files you don't want it.