Multiple collection and modes
r1m opened this issue ยท 28 comments
How shall we handle collection that references variables in another one that could use multiple modes ?
I would expect the tool to create multiple variables set but it embeds modes as token groups
I have two collections:
core:
variable | light | dark |
---|---|---|
color/base | #ffffff | #000000 |
component:
variable | |
---|---|
button/background | <color/base/brand> |
The current result is this :
{
"core": {
"light": { // <- light mode
"color": {
"base": {
"$type": "color",
"$value": "#ffffff",
"$extensions": {
"variableId": "VariableID:102:434"
}
}
}
},
"dark": { // <- dark mode
"color": {
"base": {
"$type": "color",
"$value": "#000000",
"$extensions": {
"variableId": "VariableID:102:434"
}
}
}
}
},
"component": {
"button": {
"background": {
"$type": "color",
"$value": "{core.Value.color.base}", //<- invalid reference, expected "core.color.base"
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
}
}
I know cobalt expects mode to be an extension on variables https://cobalt-ui.pages.dev/docs/guides/modes/
This plugin choosed to export modes in different files : https://www.figma.com/community/plugin/1263743870981744253/design-tokens-manager
This is still a discussion topic on design-tokens/community-group#210
@r1m thank you for detailed feedback. It looks like a bag, I'll check it out
@r1m fixed it on the version 1.6.1. Can you please check? (You might be need to reload your tab in order to get the latest plugin version)
Ok so now the alias is set to core.color.base
but this is not a valid token name because we have core.*light*.color.base
and core.*black*.color.base
.
Nothing tell us that those are variants of core.color.base. Hence my first question :) How can I write a tool that ingest the json and write multiple variation of the values ?
I hope you understand my problem
@r1m I understand. Hmmโฆ I can try to handle it like this:
"component": {
"light": {
"button": {
"background": {
"$type": "color",
"$value": "{core.light.color.base}",
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
},
"dark": {
"button": {
"background": {
"$type": "color",
"$value": "{core.dark.color.base}",
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
},
// if there is no dark or light theme, it will be ignored
"input": {
"background": {
"$type": "color",
"$value": "{core.color.base}",
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
}
What do you think?
Also I can do something like this:
"component": {
"button": {
"background": {
"$type": "color",
"$value": "{core.[modes].color.base}", // <-- indicates that there are multiple modes. Referenced by the "modes" property
"modes": ["light", "dark"], // <-- list of modes
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
}
and then you can later write your own script to handle these modes as you want
This is gonna be really hard to answer fully until design-tokens/community-group#210 is resolved, since the spec doesn't have any concept of modes/themes right now. That being said, I'd be in favor of changing the current implementation of modes in this plugin as I don't think there's a way to support aliasing without explicitly breaking the spec. The two most common options I saw being discussed most in that issue are...
- defining mode values within each individual token
- creating a separate file for each mode
I think either of these can work until the spec reaches a decision, and neither requires explicitly breaking the current spec. As was mentioned above, Cobalt's concept of modes is an example of the first approach. Cobalt could probably handle the second approach too, you'd just have to write a multiple configs and run it separately for each mode.
I have the same problem but with different token structure, so the "primitive" (core) has only one theme and the "semantic" (component) has two modes.
{
"primitive": {
"color": {
"brand-600": {
"$type": "color",
"$value": "#FFFFFF",
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
},
"brand-300": {
"$type": "color",
"$value": "#FFFFFF",
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
},
"semantic": {
"light": {
"color": {
"base": {
"$type": "color",
"$value": "{primitive.Light.color.brand-600}", //<- invalid reference, expected "primitive.color.brand-600"
"$extensions": {
"variableId": "VariableID:102:434"
}
}
}
},
"dark": {
"color": {
"base": {
"$type": "color",
"$value": "{primitive.Dark.color.brand-300}", //<- invalid reference, expected "primitive.color.brand-300"
"$extensions": {
"variableId": "VariableID:102:434"
}
}
}
}
}
}
@kkratterf thank you for reporting. Looks like a bag. I'll fix it
@dev-nicolaos @r1m what do you think about this approach?
{
"core": {
"color": {
"base":
{
"light": {
"$type": "color",
"$value": "#000000",
"$extensions": {
"variableId": "VariableID:102:434"
}
},
"dark": {
"$type": "color",
"$value": "#ffffff",
"$extensions": {
"variableId": "VariableID:102:434"
}
}
}
},
"component": {
"button": {
"background":
{
"light": {
"$type": "color",
"$value": "{core.color.base.light}",
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
},
{
"dark": {
"$type": "color",
"$value": "{core.color.base.dark}",
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
}
}
}
@dev-nicolaos @r1m what do you think about this approach?
```json { "core": { "color": { "base": { "light": { "$type": "color", "$value": "#000000", "$extensions": { "variableId": "VariableID:102:434" } }, "dark": { "$type": "color", "$value": "#ffffff", "$extensions": { "variableId": "VariableID:102:434" } } } }, "component": { "button": { "background": { "light": { "$type": "color", "$value": "{core.color.base.light}", "$description": "", "scopes": ["ALL_SCOPES"], "$extensions": { "variableId": "VariableID:416:14" } } }, { "dark": { "$type": "color", "$value": "{core.color.base.dark}", "$description": "", "scopes": ["ALL_SCOPES"], "$extensions": { "variableId": "VariableID:416:14" } } } } } } ```
The second approach is suggested by Nathan Curtis in this interesting article
(https://medium.com/eightshapes-llc/naming-tokens-in-design-systems-9e86c7444676)
@kkratterf thanks. I think it would be right to implement this approach for now, until there is a specification.
But it's a breaking change thing.
The issue I have with that proposal is that it bind the component to a specific token variation.
The idea of using different colour mode is to be able to switch the colour palette and keep the same colour construction in the overall design.
My example is simplified to give you context but I'm currently building a multi brand design with dark/light variation on each brand.
So for each combinaison the variables values are different but the relation between them is identical.
As @dev-nicolaos said, there are 2 ways of doing that.
The proposals above are all changing the variables aliases/names.
If you want to keep compliance with the spec, I think you can generate a file for each mode. Then the building tool will take care of combining them. It is the token-studio approach.
@r1m correct me if I'm wrong. This is how you want to separate modes?
// light-theme.json
{
"core": {
"color": {
"base": {
"$type": "color",
"$value": "#ffffff",
"$extensions": {
"variableId": "VariableID:102:434"
}
}
}
},
"component": {
"button": {
"background": {
"$type": "color",
"$value": "{core.color.base}"
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
}
}
// dark-theme.json
{
"core": {
"color": {
"base": {
"$type": "color",
"$value": "#000000",
"$extensions": {
"variableId": "VariableID:102:434"
}
}
}
},
"component": {
"button": {
"background": {
"$type": "color",
"$value": "{core.color.base}"
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
}
}
One per collection per mode would be even better.
- core-light.json
- core-dark.json
- component.json
Then I can combine light-component and dark-component.
Because in real life we would probably have more than one level of abstraction.
One per collection per mode would be even better.
- core-light.json
- core-dark.json
- component.json
Then I can combine light-component and dark-component.
Because in real life we would probably have more than one level of abstraction.
@r1m in this case how component.json will look like?
"component": {
"button": {
"background": {
"$type": "color",
"$value": "{core.color.base}" // what should be here?
"$description": "",
"scopes": ["ALL_SCOPES"],
"$extensions": {
"variableId": "VariableID:416:14"
}
}
}
}
And with which tool you want to build tokens then and handle files merging?
Yes the raw variable alias like core.color.base
.
I don't know which tool I would use yet๐
. I was looking at cobalt modes or styled-dictionary source/include.
Either way, I agree, it would need a transformation first.
I opened this issue because with the current implementation it is hard to extract what is a mode or a group and the token aliases are changed.
@dev-nicolaos @r1m what do you think about this approach?
I'd be opposed to this approach, as it breaks the spec by requiring the mode to be specified as part a the token path. As @r1m put it:
The idea of using different colour mode is to be able to switch the colour palette and keep the same colour construction in the overall design.
Putting each mode into a different file as you described here solves for that. Or you can take Cobalt's approach to modes and embed alternate mode values in each token's $extensions
object. Either seems fine to me until the spec has an answer.
On @r1m's idea of breaking up the tokens by both collection and mode...
One per collection per mode would be even better.
* core-light.json * core-dark.json * component.json
Then I can combine light-component and dark-component.
Because in real life we would probably have more than one level of abstraction.
I'd advise against this for now, as its relying on undefined spec behavior that's being actively discussed. Namely, whether or not it should be valid for an alias to point to a token not defined in the same file. There are a couple of open issues right now (design-tokens/community-group#123, design-tokens/community-group#166) that could result in that being explicitly disallowed.
For now I'd advocate for either...
- splitting just by mode
- If referencing tokens in other files is allowed at some point in the future, an option could be added to also split by collection
- Using Cobalt's
$extensions
approach
While I agree with all the point raised by @dev-nicolaos, I feel that generating one file per mode is not enough and would introduce a lot of duplication.
Yes, the community-group is still arguing. They are even arguing that themes/modes should be outside the token spec scope.
Let's update my example to a more realistic one.
primitives:
variable | brand1 | brand2 |
---|---|---|
color/base | #ff0000 | #0000ff |
semantic:
variable | dark | light |
---|---|---|
color/primary/background | <primitive.color.base> | #ffffff |
color/primary/text | #ffffff | <primitive.color.base> |
component
variable | - |
---|---|
background/button | <semantic.color.primary.background> |
text/button | <semantic.color.primary.text> |
Do you create 4 files ? One for each combinaison ?
I would expect something like that in the end.
/*brand.css*/
.brand1 {
--primitive-color-base: #00ff00
}
.brand2 {
--primitive-color-base: #0000ff
}
/*semantic.css*/
.dark {
--semantic-color-primary-background: var(--primitive.color.base);
--semantic-color-primary-text: #ffffff;
}
.light {
--semantic-color-primary-background: #ffffff;
--semantic-color-primary-text: var(--primitive.color.base);
}
/*component.css*/
root {
--component-background-button : var(--semantic.color.primary.background);
--component-text-button: var(--semantic.color.primary.text);
}
/*
.brand1.dark button would be white text on green background
.brand2.light button would be blue text on white background
*/
I'm using class selector for simplicity, but I could use @media (prefers-color-scheme : 'light/dark')
It that case, it is up to the developper to know which combinaison are valids and how to declare the variations. Because as nicolaos rightly pointed out : the behavior is not clearly specified.
I don't have a clear preference between separated-files/$extension-mode. I can easily write a transformation for both.
Following Cobalt implementation would help usage of this tool however ๐
My main requirement is do not change path :)
I'd like to take a shot at fixing this by implementing modes in the way Cobalt expects them ($extensions.mode
) as it seems to address all the concerns shared in this thread and it would bring these two tools into alignment. If there are concerns about not wanting to release a breaking change I could put it behind an option, otherwise this could be the default behavior.
@dev-nicolaos thank you! I'll check how I can add this into the plugin
@dev-nicolaos here is a test build with the cobalt's approach.
"container": {
"$type": "color",
"$value": "{clr-core.warn.95}",
"$description": "",
"$extensions": {
"mode": {
"light": "{clr-core.warn.95}",
"dark": "{clr-core.warn.15}"
},
"figma": {
"variableId": "VariableID:8:1902",
"collection": {
"id": "VariableCollectionId:8:1868",
"name": "clr-theme"
}
}
}
I tried the dev version today. So far, the output is exactly what cobalt is expecting. Good job ๐
@r1m Thanks! Going to publish it this weekend
Apologies, this fell off my radar as several other things came up the last few weeks. Excited to see this getting worked on though!
I do have one concern, assuming the cobalt-workaround
branch contains the proposed implementation. Currently it looks like the default value of a token is being set using the first mode listed. I think it should instead use the value of whatever mode is specified by VariableCollection.defaultModeId
from Figma's API.
@dev-nicolaos will add this
New plugin version is published
I think this issue can be closed
@dev-nicolaos Cool. Agreed, let's close it then