community-group icon indicating copy to clipboard operation
community-group copied to clipboard

Native modes and theming support

Open jjcm opened this issue 1 year ago • 59 comments

We're currently working on native support for tokens internally here at Figma. In our eyes there are two core use cases that stem from customer requests for design tokens:

  1. Token aliasing (i.e. danger-bg -> red-300)
  2. Theming

Currently the spec does not support theming, which at the moment is a blocker for us for full adoption. I'd like to start a thread here on what native mode support would look like for this format. Major shout out to @drwpow for trailblazing some of this with Cobalt-UI, to @jkeiser for turning this into a proper proposal, and to @connorjsmith for comments and critiques.

Here's the proposal we ended up with:

Overview

Modes represent alternative sets of values for a collection of design tokens. For example, one might wish to have a different value for the “background” and “foreground” tokens depending on whether they are in “light” mode or “dark” mode.

This proposal allows the user to define a set of modes that apply to all tokens in the design tokens file, allowing them to have distinct values for each mode.

Herein we’ll use this example:

{
  "$name": "Figma UI Colors",
  "$modes": {
    "light":      {}, // no fallback
    "dark":       {}, // no fallback
    "super-dark": { "$fallback": "dark" }
  },
  "bg": {
    "$type": "color",
    "brand": {
      "$value": "{colors.blue.300}", // light mode falls back to this
      "$modes": {
        "dark": "{colors.blue.500}" // super-dark mode falls back to this
      }
    }
  },
  "fg": {
    "$type": "color",
    "brand": {
      "$modes": {
        "light": "{colors.black}",
        "dark": "{colors.white}",
        "super-dark": "{colors.gray}"
      }
    }
  }
}

In this example, the values for bg and fg for each mode would be:

  light dark super-dark
bg {colors.blue.300} {colors.blue.500} {colors.blue.700}
fg {colors.black} {colors.white} {colors.white}

Defining Modes

A design tokens file may optionally define a set of named modes at the top of the file.

{
  "$name": "Figma UI Colors",
  "$modes": {
    "light": {},
    "dark": {},
    "super-dark": { "$fallback": "dark" }
  },
  // tokens ...
}

The $modes definition is an object at the top level of the design tokens file.

  • $modes should be placed before the first token or token group, to make efficient file import possible.
  • Mode names are case sensitive: "``light``" and "``LIGHT``" are different modes.
  • Mode names have the same restrictions as token and group names: they must not start with $, and must not contain {, . or } characters.
  • If $modes is empty {}, it is treated the same as if it were not specified (namely, that no modes are defined).

Fallbacks

Each mode may optionally define a $fallback mode, which will be used to determine the value of tokens which do not define a value for the given mode.

  • The lack of a $fallback value implies that mode will fall back to a token’s default $value.
        "dark":       {}, // no fallback
  • $fallback value must be the name of another mode in the same file.
        "super-dark": { "$fallback": "dark" }
  • Fallbacks must not form a cycle.
        "dark": { "$fallback": "super-dark" },
        "super-dark": { "$fallback": "dark" } // ERROR: cycle

Token Values

Design token files may specify different values for each mode, for each token.

    "brand": {
      "$value": "{colors.blue.300}", // light mode falls back to this
      "$modes": {
        "dark": "{colors.blue.500}" // super-dark mode falls back to this
      }
    }
  • A token may optionally define $value, which determines its default value.
  • If no modes are defined, $value must be defined and represents the token’s value.
  • A token may optionally define $modes, which is an object defining its value for specific modes.
  • A token’s $modes may only define values for modes defined in the same file. "$modes": {"daaaaark":"#000000"} is an error if there is no "daaaaark" mode.
  • "$modes": {} is equivalent to not specifying $modes at all.

NOTE: this relaxes the requirement that $value is required when modes exist.

Value Resolution

If modes are defined, all tokens must have values for all modes, taking into account fallback and default rules. This means that either $value or $modes (or both) must be defined for all tokens.

The value of a token for mode "m" is as follows:

  1. If the token defines a value for "m" , that value is used for the mode.
  2. Otherwise, if the mode defines a $fallback, the token’s value for the fallback mode is used. The same rules are applied for the fallback mode, so if an explicit value is not defined for the fallback mode, its fallback is used, and so on.
  3. Otherwise, if $value is defined, then that value is used for the mode.
  4. Otherwise, the token is undefined for the mode, which is an error.

jjcm avatar Mar 22 '23 23:03 jjcm

I like this approach. I am curious if mode nesting in token definitions would be supported or explicitly disabled?

For example, you have light and dark mode, but for each of those you also have increased and decreased contrast modes. Modes in this case could not be defined in isolation as there is some dependency (light + increased contrast, dark + increased contrast).

Example contrast modes:

{
  "$name": "Figma UI Colors",
  "$modes": {
    "light": {},
    "dark": {},
    "increased-contrast": {}
    "decreased-contrast": {}
  },
  // tokens ...
}

Examples with nesting:

  "text-primary": {
    "$type": "color",
    "brand": {
      "$modes": {
        "light": {
          "value":  "{colors.gray.800}",
          "increase-contrast": "{colors.gray.900}",
"decreased-contrast": "{colors.gray.700}"
        },
        "dark": {
          "value":  "{colors.gray.200}",
          "increase-contrast": "{colors.gray.100}",
"decreased-contrast": "{colors.gray.300}"
        }
      }
    }
  }

Alternatively, disallowing nesting for modes may provide a forcing function for aliasing (one level handles light/dark, another level handles contrast). Either I think are ok but should be considered.

Similarly, it may be necessary to define which modes are relative to one another, or what token types they can support. Ie, "light", "dark", and "darkest" are enumerations for a property (eg, "colorScheme"), whereas "increase-contrast", "decreased-contrast" or even "forced-colors" would be enumerations for a different property (eg. "contrastModes"). That way we can enforce that you cannot nest/combine options of the same mode property (eg, "light": {"dark": "$token"}} is disallowed).

NateBaldwinDesign avatar Mar 23 '23 03:03 NateBaldwinDesign

Today for theming we just add a new theme file with the tokens defined in the theme file overriding said tokens in the base file. This allows product teams to create custom themes without messing with the core token file.

With the modes proposal, I do not see a mechanism for defining a mode outside the main token definitions, or would it work the same way? Define the same tokens a second time with just the additional mode value?

On Thu, 23 Mar, 2023, 9:02 am Nate Baldwin, @.***> wrote:

I like this approach. I am curious if mode nesting in token definitions would be supported or explicitly disabled?

For example, you have light and dark mode, but for each of those you also have increased and decreased contrast modes. Modes in this case could not be defined in isolation as there is some dependency (light + increased contrast, dark + increased contrast).

Example contrast modes:

{ "$name": "Figma UI Colors", "$modes": { "light": {}, "dark": {}, "increased-contrast": {} "decreased-contrast": {} }, // tokens ... }

Examples with nesting:

"text-primary": { "$type": "color", "brand": { "$modes": { "light": { "value": "{colors.gray.800}", "increase-contrast": "{colors.gray.900}", "decreased-contrast": "{colors.gray.700}" }, "dark": { "value": "{colors.gray.200}", "increase-contrast": "{colors.gray.100}", "decreased-contrast": "{colors.gray.300}" } } } }

Alternatively, disallowing nesting for modes may provide a forcing function for aliasing (one level handles light/dark, another level handles contrast). Either I think are ok but should be considered.

Similarly, it may be necessary to define which modes are relative to one another, or what token types they can support. Ie, "light", "dark", and "darkest" are enumerations for a property (eg, "colorScheme"), whereas "increase-contrast", "decreased-contrast" or even "forced-colors" would be enumerations for a different property (eg. "contrastModes"). That way we can enforce that you cannot nest/combine options of the same mode property (eg, "light": {"dark": "$token"}} is disallowed).

— Reply to this email directly, view it on GitHub https://github.com/design-tokens/community-group/issues/210#issuecomment-1480552982, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEKS36B4K5ZHJNAK7WWVIKDW5O76PANCNFSM6AAAAAAWEORCFY . You are receiving this because you are subscribed to this thread.Message ID: @.***>

nesquarx avatar Mar 23 '23 05:03 nesquarx

This proposal is missing a critical bit. Since modes are user provided content translation tools can not generate code based on modes.

All possible modes must be defined by the specification for translation tools to work.


see : https://github.com/design-tokens/community-group/issues/169

romainmenke avatar Mar 23 '23 10:03 romainmenke

Today for theming we just add a new theme file with the tokens defined in the theme file overriding said tokens in the base file. This allows product teams to create custom themes without messing with the core token file. With the modes proposal, I do not see a mechanism for defining a mode outside the main token definitions, or would it work the same way? Define the same tokens a second time with just the additional mode value?

Depends on the heuristic we'd want to use for what constitutes an "extension" of the base theme (either via some sort of $extends, or just via name matching), but I'd think that defining more modes would be something like this

File 1

{
  "$name": "Figma UI Colors",
  "$modes": {
    "light": {},
    "dark": {},
    "increased-contrast": {}
    "decreased-contrast": {}
  },
  // tokens ...
}

File 2

{
  "$name": "Figma UI Colors Extended",
  "$extends": "Figma UI Colors", // Alternatively tools just batch accept files and try to combine them blindly
  "$modes": {
    "extra-dark-mode": {"$fallback": "dark-mode"}
  },
  // tokens (can overwrite values for light and dark mode, as well as define new values for the added extra-dark-mode)
}

connorjsmith avatar Mar 23 '23 14:03 connorjsmith

This proposal is missing a critical bit. Since modes are user provided content translation tools can not generate code based on modes.

All possible modes must be defined by the specification for translation tools to work.

see : #169

Could you expand on this? This proposal defines all modes upfront inside $modes, or am I misunderstanding the problem?

A minimal example would be super helpful, thanks in advance!

connorjsmith avatar Mar 23 '23 14:03 connorjsmith

A minimal example would be super helpful, thanks in advance!

The json in the first post is an excellent example.

How would a translation tool process that file? How do end users "access" the mode specific values?

Taking CSS as an example. What would the generated CSS code be for that file?

romainmenke avatar Mar 23 '23 14:03 romainmenke

Also see : https://github.com/design-tokens/community-group/issues/204

romainmenke avatar Mar 23 '23 15:03 romainmenke

I would expect the following generated CSS

/* figma-ui-colors_light.css */
:root {
  --bg-brand: #1010FF; /* colors.blue.300 */
  --fg-brand: #000000; /* colors.black */
}

/* figma-ui-colors_dark.css */
:root {
  --bg-brand: #1010FF; /* colors.blue.500 */
  --fg-brand: #FFFFFF; /* colors.white */
}

/* figma-ui-colors_super-dark.css */
:root {
  --bg-brand: #0000FF; /* colors.blue.700 */
  --fg-brand: #A0A0A0; /* colors.gray */
}

Usage:

@import url("figma-ui-colors_light.css") prefers-color-scheme: light
@import url("figma-ui-colors_dark.css") prefers-color-scheme: dark

Those variables would be split up over 3 files, one for each mode. Alternatively a generator could specify all modes in a single file

:root {
  --bg-brand: #1010FF; /* colors.blue.300, from $value */
}

:root[mode="light"] {
  --bg-brand: #1010FF; /* colors.blue.300 */
  --fg-brand: #000000; /* colors.black */
}

:root[mode="dark"] {
  --bg-brand: #1010FF; /* colors.blue.500 */
  --fg-brand: #FFFFFF; /* colors.white */
}

:root[mode="super-dark"] {
  --bg-brand: #0000FF; /* colors.blue.700 */
  --fg-brand: #A0A0A0; /* colors.gray */
}

Of course tools could probably optimize the above to utilize css var override cascades

Another alternative would be for tools to append the mode to token names, should a "multi mode" or override use case be required

  --bg-brand-light: #1010FF; /* colors.blue.300 */
  --fg-brand-light: #000000; /* colors.black */

  --bg-brand-dark: #1010FF; /* colors.blue.500 */
  --fg-brand-dark: #FFFFFF; /* colors.white */

  --bg-brand-super-dark: #0000FF; /* colors.blue.700 */
  --fg-brand-super-dark: #A0A0A0; /* colors.gray */

Are there other gaps that a generator tool wouldn't be able to create based on the proposed JSON structure?

connorjsmith avatar Mar 23 '23 16:03 connorjsmith

Looking at the postcss plugin, I'd expect this to be possible with an added mode() extension. The mapping could be done in postcss config as well, similar to the is option

Using new mode() option

@design-tokens url('./figma-ui-colors.json') format('style-dictionary3') mode('light');
@design-tokens url('./figma-ui-colors.json') when('brand-2') format('style-dictionary3') mode('dark');

.foo {
  color: design-token('fg.brand');
}

Using postcss config

postcssDesignTokens({ 
  modes: { 
    'figma-ui-colors.json': 'dark'
  } 
})

connorjsmith avatar Mar 23 '23 16:03 connorjsmith

I think there is some confusion here :) Is this a proposal for "theming" or for "conditional/contextual values"?


Theming :

  • one design system
  • multiple sets of values
  • multiple outputs
  • static result

Examples :

  • a customizable design system that is used by 3rd parties
  • having variants of a design in multiple color hues (a blue theme, a red theme, ...)
  • ...

Conditional values :

  • one design system
  • multiple sets of values
  • one output
  • dynamic result
  • native API's

Examples :

  • dark mode
  • light mode
  • high contrast mode
  • large screens
  • small screens
  • ...

It might sometimes be possible to build a dynamic result by combining multiple themes on a single page or screen but these are always custom mechanics that don't leverage native API's.

Given the mentions of dark mode and high contrast mode I assumed this was a proposal for conditional values.

Can you clarify?


If this is a proposal for theming and isn't intended for dark mode / light mode then we would just wire this up behind is in the PostCSS plugin.

For a tokens file with modes a and b defined within a single file :

@design-tokens url('./figma-ui-colors.json') format('style-dictionary3');

.foo {
  color: design-token('fg.brand');
}

output A :

postcssDesignTokens({ 
  is: ['a']
})

output B :

postcssDesignTokens({ 
  is: ['b']
})

But that is only interesting for us to do when this feature isn't intended for dark / light mode and other conditional values.


This proposal doesn't make it possible for us to support this :

CSS author writes :

.foo {
  color: token('foo');
}

We generate :

.foo {
  color: red;
}

@media (prefers-color-scheme: dark) {
  .foo {
    color: pink;
  }
}

We can not generate this output because there isn't any link between a mode with user defined name "dark" and the concept of a functional dark mode.

romainmenke avatar Mar 23 '23 17:03 romainmenke

That kind of link can, however, be supplied to the code generator as configuration. Every code generator tool is going to require some amount of configuration to make the output fit well with your codebase; a per-output-platform mapping of mode names to "the native thing" could be part of that configuration.

{
  "light": "",
  "dark": "@media (prefers-color-scheme: dark)"
}

TravisSpomer avatar Mar 23 '23 17:03 TravisSpomer

Put another way, even if this spec defined a full set of what the allowed themes/modes were, and they were light and dark, a CSS code generator would still need to know how to map those themes to code. One project wants @media (prefers-color-scheme: dark), another makes dark mode the default and uses @media (prefers-color-scheme: light), and another uses [theme-dark] and switches attributes with JavaScript.

TravisSpomer avatar Mar 23 '23 17:03 TravisSpomer

[theme-dark] That is not a native API for dark mode and shouldn't be taken into consideration. Anyone can do any custom thing.

One project wants @media (prefers-color-scheme: dark), another makes dark mode the default and uses @media (prefers-color-scheme: light)

That would just work. Your base set of values wouldn't be wrapped in any conditional at rules. Your conditional set of values would be wrapped in whatever conditional matches.

If a designers decides to do dark as default then the code generator would produce :

.foo {
  color: pink;
}

@media (prefers-color-scheme: light) {
  .foo {
    color: red;
  }
}

But I don't want to focus to much on conditional values and the benefits of being to generate those without first confirming if the original proposal was for theming or conditional values.

romainmenke avatar Mar 23 '23 17:03 romainmenke

Sorry for the confusion! Yes this is purely about theming/modes, not with determining how/when those modes should be applied.

connorjsmith avatar Mar 23 '23 19:03 connorjsmith

Since modes are user provided content translation tools can not generate code based on modes.

All possible modes must be defined by the specification for translation tools to work.

I heavily disagree with this. Any list that we come up with of valid modes will never satisfy the demands of end users. Let's say for example we have these modes as valid ones:

  • Light
  • Dark

But now a user wants to add midnight mode (i.e. similar to dark, but all near-blacks are set to black to preserve battery life on OLED mobile devices, a fairly common pattern). Are we saying that midnight modes are an invalid use case for tokens?

Maybe we add it, so now our list is:

  • Light
  • Dark
  • Midnight

But now a user wants to add high contrast. Another user wants to add colorblind mode. Lets say somehow we hypothetically come up with a list of modes that encapsulate all possible visual modes (which personally I don't believe is possible, but lets say hypothetically we did) and we ended up with something like,

  • Light
  • Dark
  • Midnight
  • Deuteranopia
  • Protanopia
  • Tritanopia
  • High Contrast
  • Low Contrast
  • ...etc

But a large organization wants different themes per product brand, one of which has a light appearance with a red brand color, and another of which has a dark appearance with a green brand color. Are we saying that's an invalid use of tokens?

Modes need to be user defined, not an enum.

The output of these modes by translation tools should attempt their best-guess, but there are hundreds of ways people represent theming on the web today - there's no standard for it (aside from prefers-color-scheme, which we already know doesn't satisfy the needs of users).

jjcm avatar Mar 23 '23 20:03 jjcm

That kind of link can, however, be supplied to the code generator as configuration. Every code generator tool is going to require some amount of configuration to make the output fit well with your codebase; a per-output-platform mapping of mode names to "the native thing" could be part of that configuration.

{
  "light": "",
  "dark": "@media (prefers-color-scheme: dark)"
}

I like this, though this also feels like something that might belong more in $extensions rather than the core format, though I'm curious what others think here. IMO the core format should be apathetic to the output format.

jjcm avatar Mar 23 '23 20:03 jjcm

@jjcm https://github.com/design-tokens/community-group/issues/210#issuecomment-1481853876

Please see my comment here : https://github.com/design-tokens/community-group/issues/210#issuecomment-1481554248


Why not have both generic themes and conditional/contextual values?

The core concept of this design tokens specification is to be a bridge between different formats. Part of that work imo is mapping concepts like native API's for dark mode.


IMO the core format should be apathetic to the output format.

I disagree with this.

The format should try to be platform agnostic but it must be sympathetic to how it will be used.

If a small change to the format can make a large difference on the output for all platforms then that seems like a worthwhile thing to me.

romainmenke avatar Mar 23 '23 21:03 romainmenke

I really like this proposal and think building theming into the format will improve interoperability.

At Interplay, we use a very similar data structure internaly and get users to import/export specific themes to the token group format. The only difference is we use $values rather than $modes, and $inherit rather than $fallback

One suggestions I have is making $fallback an array instead of a single value.

Then users could define themes/modes for compact, jumbo, dark and light. Then they can combine these to create e.g. dark-mobile, light-mobile, etc. The value of the tokens would be determined by the order of the inherited themes.

This would save a lot of duplicate entry for values across themes.

{
  "$name": "UI Colors",
  "$modes": {
    "light": { }, 
    "dark": { },
    "compact": { },
    "jumbo": { }, 
    "dark-compact": { "$fallback": ["dark", "compact"] }
  },
  "font-size": {
    "$type": "dimension",
    "small": {
      "$value": "1rem",
      "$modes": {
        "compact": "0.5rem",
        "jumbo": "1.5rem",
      }
    }
  },
  "backgrounds": {
    "$type": "color",
    "base": {
      "$value": "{colors.white}",
      "$modes": {
        "light": "{colors.white}",
        "dark": "{colors.black}",
      }
    }
  }
}

The values for these tokens in the dark-compact theme would be font-size.small: 0.5rem backgrounds.base: {colors.black}

mkeftz avatar Mar 23 '23 21:03 mkeftz

@mkeftz interesting proposal with the array, totally see the use case.

One thing I'm curious about though is if others expect to define their dimension modes along with their color modes. Having compact be a valid theme for UI Colors feels somewhat weird to me. I'd expect each of these to be in separate files given the lack of overlap in most situations. Would be curious to see what others think.

jjcm avatar Mar 23 '23 22:03 jjcm

Perhaps mobile/desktop and compact/large would be a more common combo? E.g. certain buttons would likely want their mobile sizings (for us fat fingered individuals), but non-interactive densities would want to stay compact (with the occasional explicit mobile-compact value where those two would overlap).

Mainly spitballing though, I'd also be curious to see how other people would plan on using that!

connorjsmith avatar Mar 24 '23 01:03 connorjsmith

This topic requires a lot of research, or basically the one I did for the past years and started to put into blog posts. I'm currently nearing ~10k words (so there is a substantial backup for what's to come). I hope to start releasing this series in late april.
With the spoiler out of the way that there is more to come, I think this topic is very important and I can share parts I have ready already.

Features / User Preferences

Let's start this from user preferences, this is what a user might wanna choose:

user-preferences

And the idea is, whatever the user is about to choose, will receive the respective value for that token. As theme authors we would call them features. A theme supports feature color-contrast or color-scheme, etc. Not all of these can be "System", as these may be product related options. See next section for more differentiation.

By the way, here is github:

Bildschirmfoto 2023-03-28 um 18 15 21

They don't have a "skin", but support all the other features from the user preferences menu above (in terms of color). You can even have dark appearance set when your system is light o_O.

Behavior

There is a behavior involved in here:

  • Adaptive: User chooses "System" and passes the choosing on to the Operating System
  • Mode: User explicitely chooses a particular preference

In CSS:

:root {
  --background: white;
}

/* Adaptaive */
@media (prefers-colors-scheme: dark) {
  :root {
    --background: black;
  }
}

/* Mode */
[theme-color-scheme="dark"] {
  :root {
    --background: black;
  }
}

The important thing: The behavior goes independent from the storage of values in the tokens file! This goes into the next step when values are extract from the tokens file and put into CSS or whatever else format

References as Solutions in Development

In development we use references as solutions to this problem, here is one for colors:

References as Solutions

With that configuration above that is:

intent-action-base-background = 4 x 4 x 3 x 3 = 144

so the token can take 144 permutations in this example - if there was a value for all of them (which in reality I wouldn't expect).

A Format to Structure Permutations

Modes is the wrong word to this - as this is something a user would opt into (see Raskin, J. (2008). The Humane Interface: New Directions for Designing Interactive Systems (10th printing). Crawfordsville, Indiana: Addison-Wesley.) For example nowadays it would be dark color-scheme and not dark mode (that goes back to the old days, when it really was a mode - it still is but has moved to OS level).

I'm also not sure yet, what would be the best format to support this. It however needs to be defined on the token itself. It needs to be stored alongside the feature configuration. For example:

{
  "$name": "intent-action-base-background",
  "$value": [
    {
      "$value": "white"
    },
    {
      "$value": "darkgrey",
      "color-scheme": "dark"
    },
    {
      "$value": "black",
      "color-scheme": "dark",
      "color-contrast": "high"
    }
  ]
}

The finding of tokens would be programmatical. Those with the highest match win:

const token = tokens.find(t => t.name === 'intent-action-base-background');
const value = token.value.find({ 
  colorScheme: 'dark', 
  colorContrast: 'high' 
});

That is in fact no different than how tools currently work, as danny wrote in https://dbanks.design/blog/dark-mode-with-style-dictionary (Single Token Method)

An alternative to stick with the object approach would be (DON'T DO THIS):

{
  "$name": "intent-action-base-background",
  "$value": {
    "": "white",
    "dark-": "darkgrey",
    "dark-high": "black"
  }
}

where features become the key within $value (This is how one needs to do that in figma right now, because styles don't support multiple values... hint hint :D). However, I'm heavily against this, as it would eliminate structural information and requires parsing.

In terms of scalability a format is required that can potentially scale up to ~150 permutations per token but we also know there will be maybe permutations based on 2 features with 2 permutations - as companies will already started to support color scheme and soon color contrast is to come (maybe because it only has two options? or because it has media query backup?). But once this wave is over, we probably will see adoption for chroma - Yes I'm very hypothetical here.

On the other hand the format shall be practical, this is why all (?) ideas so far use an object. This could easily end up in a nightmare nested tree as @NateBaldwinDesign showed (given the github use-case above) and practicability is gone.

I'm still with the array and it's configuration, but happy to read about better ideas.


Also, if I rushed over some of the topics, then please ask for more detailed explanation.

gossi avatar Mar 28 '23 16:03 gossi

so the token can take 144 permutations in this example - if there was a value for all of them (which in reality I wouldn't expect).

Definitely agree! That's one of the reasons why we are pushing for fallback definitions for each mode, as well as overrides being optional. You shouldn't have to define all permutations.

Modes is the wrong word to this

Modes is the wrong word if you're only supporting color. What complicates things is that design tokens are used for many use cases, color just being one of them. We originally were using theme and color-scheme, but we found it didn't apply for other use cases of tokens. Here are the use cases we came up with that we felt the terminology should support:

  • visual themes (i.e. light/dark)
  • product/brand themes (i.e. docs/sheets/slides)
  • accessible themes (high contrast, colorblind modes)
  • dimensions and platform size differences (mobile/desktop/tablet)
  • UI string translations / alternatives (i.e. english/spanish/german)
  • and likely more in the future! What happens with depth is a factor for VR/AR?

In the end we found that color-scheme simply didn't work outside of color (i.e. a mobile color scheme doesn't convey that you're talking about differences in padding), anything that's specific like that also encounters issues of not being scalable for future use cases. theme was great for color as well, but it didn't work well for things such as translations (i.e. a "German theme" feels like you're going to have a bavarian/oktoberfest vibe, not having strings translated to German). We ran a survey among a few different disciplines and users, and found that mode was the least-bad option for a generic term. It was the 2nd choice among the 3 cohorts we tested: visual designers, content designers, and those working explicitly on dimensionality - not their first choice, but one that still made sense for their use case.


The permutation structure you suggest is interesting - treating each permutation as a flag rather than an individual grouping. I'm not a huge fan of the readability of it (though I'm not sure if that matters for an interop format), but I see the value. Would be curious on the needs of this vs explicit modes. One thing to call out is with this approach you'd need some very explicit logic around missed finds, as with this approach it's possible for no correct value to exist.

As an example, if you had:

{
  "$name": "intent-action-base-background",
  "$value": [
    {
      "$value": "white"
    },
    {
      "$value": "darkgrey",
      "color-scheme": "dark"
    },
    {
      "$value": "black",
      "color-scheme": "dark",
      "color-contrast": "high"
    }
  ]
}

And you queried for:

const token = tokens.find(t => t.name === 'intent-action-base-background');
const value = token.value.find({ 
  colorContrast: 'high' 
});

Would you return black or white in this case? There's no explicit definition for color-contrast: high without dark mode defined. Do we then return the dark mode value? Or do we return the light value?

If invalid queries like this aren't allowed, validations would be non-trivial as they'd be exponential in nature to test. A flag based approach is definitely going to need more thought.

jjcm avatar Mar 29 '23 23:03 jjcm

Thanks for sharing this thorough proposal @jjcm, and thanks everyone else for all the insightful comments here!

Overall, I really like this proposal. I think it would provide the format with an elegant mechanism for providing alternate token values for any number of use-cases. As Jake pointed out in the previous comment, this could have applications way beyond light and dark color schemes.

Of course, since the modes are author-defined (i.e. there wouldn't be a pre-defined set of permitted mode names in the spec), tools can't automatically infer any semantics from the mode names or apply specialised functionality based on the selected mode.

If I understood correctly, I think this is essentially the issue @romainmenke raised in his comments. For example, a translation tool could not "know“ that a particular mode represents the colors that should be used when the user has set their OS to dark mode and therefore could not automatically output an appropriate @media query in CSS.

However, I think the $modes proposal nonetheless enables a lot of use-cases that are so far impossible to represent using the current DTCG format draft. I'd therefore be in favour of adding it to the spec.

I do think some of the finer details need ironing out first though...

Does $value really need to be optional?

The OP proposal relaxes the requirement for every token to have a $value since could instead just have $modes. While this certainly could be done, I wonder whether it might keep things a bit more consistent and simple if we keep $value being mandatory.

The way I see it, every DTCG file has an implied "default" mode, which is what you get if you didn't use $modes at all. So why not lean into that and encourage authors to only use $modes for additional modes they might need.

Taking the initial example from the OP, we might decide that "light" is our default and therefore only use $modes to create the additional "dark" and "super-dark" modes. The example could then be rewritten as:

{
  "$modes": {
    // no longer specify light
    "dark":       {}, // no fallback
    "super-dark": { "$fallback": "dark" }
  },
  "bg": {
    "$type": "color",
    "brand": {
      "$value": "{colors.blue.300}", // light mode falls back to this
      "$modes": {
        "dark": "{colors.blue.500}" // super-dark mode falls back to this
      }
    }
  },
  "fg": {
    "$type": "color",
    "brand": {
      "$value": "{colors.black}", // use $value rather than $modes.light here
      "$modes": {
        "dark": "{colors.white}",
        "super-dark": "{colors.gray}"
      }
    }
  }
}

I think doing it this way and retaining the "every token must have a $value" rule is preferable for the following reasons:

  • Avoids situations where an author omits $value but forgets to specify $modes values for every mode. In that case there'd be no $value to fall back on, so the token becomes... invalid?
  • Could make the learning curve for the format lower since there's less exceptions to learn. I think "every token must have a $value" is simpler than "every token must have a $value, unless it has a $modes property". Especially if you consider $modes an advanced feature that newcomers might not learn about right away.
  • Keeps parser logic a (tiny bit) simpler too for the same reason as above.

I think the only thing we might lose would be the ability to assign a name the default mode. In the above example, users would need to know that "no mode means light mode". However, we could solve that by having a means to provide optional metadata about the default mode. For example, we could have an optional $defaultMode property that could be added to the top-level $modes declaration like so:

{
  "$modes": {
    "$defaultMode": {
      "$name": "light"
    },
    "dark":       {}, // no fallback
    "super-dark": { "$fallback": "dark" }
  },
 // ...
}

Could modes have additional metadata?

For example $description? I could imagine this might be useful for tools that have the ability to list out all declared modes to the user.

{
  "$modes": {
    "$defaultMode": {
      "$name": "light",
      "$description": "A color scheme to be used when the user has expressed a preference for a light UI"
    },
    "dark": {
      "$description": "A color scheme to be used when the user has expressed a preference for a dark UI"
    },
    "super-dark": {
      "$description": "A even darker version of the dark color scheme which uses pure black backgrounds to preserve energy on devices with OLED displays",
      "$fallback": "dark" 
    }
  },
 // ...
}

It's conceivable that future spec iterations might add more properties for other bits of useful metadata for tokens and groups. When doing so, we could also consider whether they might be useful for mode declarations and, if so, permit them to be used here.

Should modes have $extensions?

Sort of related to the previous point, maybe permitting $extensions on mode declarations could be useful too. The use-case would be extensions that relate to a mode itself rather than a token or group.

This kind of thing could perhaps enable tools to add some additional semantics per mode, as @TravisSpomer was suggesting in response to @romainmenke.

E.g.:

{
  "$modes": {
    "$defaultMode": {
      "$name": "light",
      "$extensions": {
        "org.postcss.design-tokens.media-query": "(prefers-color-scheme: light)" 
      }
    },
    "dark": {
      "$extensions": {
        "org.postcss.design-tokens.media-query": "(prefers-color-scheme: dark)" 
      }
    },
    "super-dark": {
      "$extensions": {
        "org.postcss.design-tokens.media-query": "(prefers-color-scheme: dark) and (prefers-contrast: more)" 
      },
      "$fallback": "dark" 
    }
  },
 // ...
}

Obviously, the standard $extensions caveat of being prorietary and thus not universally supported by tools applies. But this could still provide an "escape hatch" to enable tool-specific functionality. Also, if particular extensions gain a lot of adoption, it provides us with a cowpath to pave in future spec iterations.

c1rrus avatar Mar 30 '23 08:03 c1rrus

Oh, so wonderful thoughts in your answer :)

PS. While typing my answer @c1rrus also posted (I will read his post after I posted mine). This is a response to @jjcm

I think, permutation (as odd as it sounds) is the word I used most for to describe those varying token values. It is not as much as opinionated as mode - wdyt? Perhaps not necessary, as values might be best as an array?! 😅


I thought about validation and fallback a bit more after posting here. Here is a bit more of what's in my head.

Do we want to have a fallback for the sake of having a fallback, because we want to have it or because it must be there? I think we are going with fallback, because we want to have it to avoid dealing with the complexity that awaits us. And is fallback even a correct value then?

About features, these are defined by your product/theme/design system - they may or may not live within the token file. The bonus of having it is to that you can validate the file by itself, which I'd actually in favor of having - or this can be set as reference:

{
  "$reference": "path/to/features.tokens"
}

I think, having validation is important. I'm author of theemo and am currently working on v2 which actually is about letting designers define their features and then provide a UI for letting them define values for a token based on features - sounds familiar?

Here is the challenge: Let's say you defined two features, color-scheme (light/dark) and color-contrast (low/high) and you have a color token with a given value. Now you want this token to support color-scheme. Your UI splits into two input fields one for light one for dark and copies over the value from before into the two new input fields. Let's say the value from before was set for light color-scheme, the designer now will choose a value for dark color-scheme. The token stays valid as long as there are two values present. The original value before this token support color-scheme feature is no longer relevant (=> no fallback needed, the fallback became the light color-scheme value). The UI can validate if the token has support for all values based on the features the token shall support.

Next up: Let's make this token support color-contrast feature. We will be presented with a 2 x 2 input matrix. Same drill again for the designer. However, if low contrast was considered the default, then the designer would only change the values for high color-contrast. The values for low color-contrast can even be omitted. This is the "none" case in the picture above. The fallback here are the two color-scheme values without color-contrast.

That is a token is valid as long as permutations of a supported feature are matched with permutations of other supported features - hell, this is a heck to explain in words. I'm having truth tables in my mind here, where you can cluster groups.

What's important to note here is, that features will have a default, which would ease building UIs and help validate (from my research before, I never find a case where there is no default, this will always ever be provided by someone - the OS at last). Let's say our default for color-scheme is light and revisit the process from above. The designer would choose to support color-scheme for a token, the previous value is copied into light color-scheme (the default) and leaves the designer to fill out only the dark value. Same for color-contrast set to low as default, when supporting color-contrast on the token, the UI would only show the option to the designer to fill out values for high color-contrast.

Which brings us back once more to fallbacks: The fallbacks are the default values of a feature.

That is a fallback for the entire value set is wrong, think this: If you have a fallback value (that was set for light color-scheme and low color-contrast) and as a user you want to experience a product in dark color-scheme and high color-contrast. If there is a value given for dark color-scheme but low color-contrast and the fallback value - which one to serve to users?

Which brings us to your question:

Would you return black or white in this case? There's no explicit definition for color-contrast: high without dark mode defined. Do we then return the dark mode value? Or do we return the light value?

I think there are two options here to answer this:

  1. That's an invalid token, as the request you are having cannot be answered - because explicitely matching your query does not work
  2. Assume the default for colorScheme and combine it with the query for colorContrast to return the appropriate value

When authoring (build time) we are in need of defining a default (this is truly a mode * here). Whereas in experiencing a product (runtime) as a user, a preference is given (by the the default from the product, by the platform (browser) or latest by the OS). With that preference present the correct token value can be found.

* maybe this was the case for calling it mode?


I have been playing around with typing this to work on theemo - as typing is a good way to prototype this and figure out problems. In the next branch (I gave it a push right now), there are types in @theemo/core - I've been playing with only the features the web offers through media queries (color-scheme and color-contrast), but potentially could be more.

A theme in theemo would be to it's current state defined as:

{
  "name": "super theemo",
  "features": {
    "color-scheme": {
      "behavior": "adaptive",
      "default": "light"
    },
    "color-contrast": {
      "behavior": "mode",
      "default": "low"
    }
  } 
}

behavior is for the implementation on the product (ignore, see post above) but default will be used for constructing the UI for designers to pick a proper value as well as the algorithm to find the correct token value.

gossi avatar Mar 30 '23 09:03 gossi

Could modes have additional metadata?

For example $description? I could imagine this might be useful for tools that have the ability to list out all declared modes to the user.

Should modes have $extensions?

Sort of related to the previous point, maybe permitting $extensions on mode declarations could be useful too. The use-case would be extensions that relate to a mode itself rather than a token or group.

This kind of thing could perhaps enable tools to add some additional semantics per mode, as @TravisSpomer was suggesting in response to @romainmenke.

Both of these suggestions seem like sensible considerations to add, as both fit existing patterns within the spec.

The use of $extensions to handle generator considerations seems like a particularly appropriate approach, as how tokens are output may have different solutions even within a particular platform or framework. Consider how one might handle modes and themes in CSS, there are several strategies one could employ. Just off the top of my head, one could:

  • Generate a single CSS file containing all permutations; each permutation wrapped within an appropriate media query
  • Generate a single CSS file containing all permutations; each wrapped within a rule and use some matchMedia logic to toggle rules as needed
  • Generate CSS files for each permutation and use the media attribute of the link or style elements to responsively load files
  • Some combination of all of the above

Moreso, one might desire to optimize the token output any number of ways, perhaps choosing to output a set of default/base tokens that are stable across all permutations paired with streamlined sets per permutation that only contain the unique tokes for that permutation OR simply render all tokens per permutation. This choice may even be different per targeted platform, technology, or framework.

It seems prudent to me, that the DTCG spec should aim to provide the means for a designer/team to model the relationship of the tokens to modes, but leave the business of how tokens are translated to generator tools. Leveraging the $extensions field to provide instructions for generator tools accomplishes that very well.

jeromefarnum avatar Apr 01 '23 18:04 jeromefarnum

It seems prudent to me, that the DTCG spec should aim to provide the means for a designer/team to model the relationship of the tokens to modes, but leave the business of how tokens are translated to generator tools. Leveraging the $extensions field to provide instructions for generator tools accomplishes that very well.

As a translation tool implementer I can safely and surely say that we will never go beyond the specification. If there is an expectation of some behavior in translation tools, it needs to be specified. We don't see the point of building tools for a specification and then having to invent critical implementation details.

I think it is dangerous to consider $extensions as an escape hatch that can be used to fill in holes in this specification. We have zero interest in keeping track of what other tools are dumping in $extensions and building logic around that.

$extensions is only realistically useful to store data to be used by a single tool or multiple tools by a single vendor. It isn't suitable as a basis for complex features that must work in all tools.

I am still hoping that this format looks at the whole picture, design tools and translation tools.

romainmenke avatar Apr 01 '23 19:04 romainmenke

Linking to #187 to group all theming related discussions together.

kevinmpowell avatar Apr 03 '23 16:04 kevinmpowell

Hi folks, long time no visit. @kevinmpowell pointed me in the direction of this thread to weigh in.

Note

tl;dr, I recommend different files for different contexts (themes) and I have no idea how the group could create a schema for token aliasing because humans have opinions on what's best for them.

One of the things I'm concerned with here depth that this is introducing, and not a good depth in my opinion. Both in terms of variation and in terms of diving into an object for values. The former is difficult for humans, the latter is challenging to code. Specifically about coding to a complex object spec; diving into trees several levels deep with variations of keys looking for values sounds like an engineering interview question I have in my nightmares.

What I'd like to propose is aligned with the way I believe humans think of token assignment. We aren't thinking about light and dark mode simultaneously, we think of them one at a time. Therefore, the first step to my recommendation is to first focus on solving the theme layer; which I believe should be a nearly flat structure of semantic token to value assignments. No variation of light and dark, because each file is meant to relate to a single context. Here's a very minimal example of what could be considered a "light" theme based on the spec today:

{
  "ux-surface-backgroundColor": {
    "$type": "color",
    "$value": "white"
  }
}

As a human thinking about how I want to assign color semantically, I scope myself to a single context (ie., mode/business-need/experiment) at a time. If I'm working on a light theme, I'm not trying to find all of the light theme values in a single file, I'm working in the file that is meant to convey the light theme. Anything else is just noise. Granted, I recognize that the expectation is to not work with token files directly but I believe there's an opportunity for simplicity here. Defining each theme file as a single context helps focus the curation exercise. Having everything available at once is triggering Hick's Law. Even with tools, this would be visually daunting. Speaking from experience here, if we were to attempt to put all tokens across all brands in the same file at GoDaddy (with numerous reseller brands), we're talking about a number around 24,000 token assignments in a single file or hidden within the depths of nested UI.

Furthermore, this supports the ability to have a scoped theme within a larger page. You can be sure that anything placed within that scope (ie., inverted hero) will be covered either by the page level theme, or the one expected to be applied in scope. There's more about that in this post, where I recommend that variations of "on dark" as token names are not scalable.

What I've mentioned above covers semantic token to value assignment; the tokens that will eventually apply directly to UI elements. What it doesn't cover is additional tokens that I recognize would be helpful for brand organization. In my view this layer is wildly unique among teams, brands, and organizations because it is often a reflection of the personalities of the people who maintain these layers.

I ask myself why people need additional layers all the time. In reality there's nothing stopping someone from just assigning blue as the value to a semantic token. Again, speaking as someone who has been making these kinds of value assignments for years, creating a color.blue.500 token just means I'm copying (or assigning through some tool) the color.blue.500 token over and over across them theme layer in the same place I would be copying blue. In reality color.blue.500 shouldn't change because we don't easily know what this means for the assignments at the theme layer. Seeing the change of color.blue.500 in a sample of UIs doesn't cover what it means for all UI as a semantic tier does.

The only reason I can think of that these other tiers could exist is to be able to speak about the token conversationally. It's clearly more helpful to say color.blue.500 over #1d9bf0, there's even some categorization built into the name. It feels helpful from a human perspective.

Above lies the challenge, as I imagine it'll be impossible to propose avoiding additional tiers. It'll be more of a challenge to define what these tiers look like and support all of the variations the humans may dream up (I can see the marketing team coming in there, wanting a color called make-it-pop-pink).

From there, it's a matter of importing this tree into the theme file:

import colors from './colors.json';

// OR, and probably more desirable for multibrand

import { colors } from './my-brand-styles.json';

// For the "light" theme
const tokens = {
  'ux-action-backgroundColor' : {
    '$type': 'color',
    '$value': colors.blue['500']
  }
}

export default tokens;

I'm admittedly torn having an opinion at the additional tier layers (aka token aliasing). On the one hand as a specification meant to cover tokens, it should have some well-defined schema where systems can share, read, and manipulate maintaining expectations. However, on the other hand, I find these additional tiers mostly useless personally. Which probably answers the question for the group about whether to include them or not. Clearly they should be included for the greater design community, I just can't imagine how we're going to cover everything people could want to do here in a schema without it being a dynamic dumping ground or tied up in ego.

I'll also admit that I lean into the semantic layer hard. I believe that UI designers are really theme authors; people interested in what the values of the UI are. And that UX designers (folks interested in the user experience) could submit wireframes which are wired semantically (this is a button, this is a card) to point to the semantic tokens to be informed by the theme author. This presupposes that UX designers don't have opinions about what color their design should be, and I know that's absolute crazy talk.

ddamato avatar Apr 09 '23 04:04 ddamato

I think I'm aligned with @ddamato's feedback here, but I'm wondering if there's some assumptions being made about the token authoring process that are influencing these proposals. I don't want to derail this thread, but I think it's useful to do some context setting.

Should a tokens file be easily edited/maintained by a human in a text editor?

#149 is still sitting open...and @romainmenke has posed some follow up questions we've yet to respond to there.

A. If yes - a tokens file should be easily edited/maintained by a human in a text editor

Then I see the issues of interleaved modes/themes within a tokens file as hugely problematic. I believe token authors will be most interested in maintaining how different tokens interact within the same theme, rather than focusing on a single token's value across themes.

B. If no - tokens files will almost never be edited/maintained by a human in a text editor

Then a design tool like Figma or Sketch, or some other token management tool could easily parse the file, and allow me to filter my tokens file down to the context of a single theme, regardless of the underlying structure within the tokens source file.

I think this is an important distinction, and I sense most of the proposals here are assuming B, but I'd love clarification on that from @jjcm @gossi @c1rrus and others.

Multi-file approach

I think @dbanksdesign's explorations and conclusion in this article and @ddamato's examples above are worth considering here.

I could imagine something like this, where the responsibility for naming themes, defining fallbacks, etc is something a token tool handles, but not something that requires that complexity to be added to the token files themselves. To me, the benefit is simpler (and smaller) token files to maintain, organized by context. If a token editor needed to check the different values for a single token across files, a token tool could aid that use case, but I'm assuming that's needed less often than working across tokens within a context.

image

Feedback

I'm interested to hear arguments against the multi-file approach and what the drawbacks might be. I haven't seen this method discussed in detail in this thread yet.

And great discussion everyone! Really appreciate the community involvement here.

kevinmpowell avatar Apr 13 '23 14:04 kevinmpowell

I'm reading through this, but especially on @ddamato's post I came to realize we've been using very overloaded terms and everybody is defining them differently, which in turn makes it complicated for us to properly communicate (as I don't really know what Donnie means with theme or context). I consider myself part of this problem and started to define them for my blog series.

I start with definitions at first, then I have an excerpt for you, that is one of the articles of the series (which is about theming, which I actually started writing before I know about this issue) - but is exactly targeting this issue. And later I propose this into a something formalized that has draft spec character for us to discuss and invent further.

1. Definitions

(Design Token) Feature

Features provide a customization option for users to modify their experience.

Features encode user preferences (e.g. color scheme or color contrast). Design tokens may or may not support a feature.

Theme

Themes contain the design decisions of your brand with differing capabilities (features and scopes) and are exchangeable within your product.

A theme defines its capabilities and delegates implementation of these to its tokens.

(Design) Token Designers

A token designer develops designs tokens. The token design includes references, possible transforms, ambients and computed tokens (this can be done by designers and/or developers !).

(Design) Token Consumer

A user applying a token - most likely designers and engineers, but also includes branding and marketeers or generally speaking people applying the brand by using design tokens.

(Design) Token Users

Token users include token designers and consumers, it's a way to refer to both parties. And includes the fact a token designer here, can be a token consumer there.

Tooling Authors

Folks to support design tokens in their tools and define the processes we use for crafting.

(End)Users/Customers

People like you and me visiting apps and sites and experience products applying design tokens for their theme.

Product

The application, website or service an enduser is using. A product uses a design system.

2. Theming with Design Tokens (an Excerpt)

The definitions above seem a little exaggerated, but I use them in the article, which dives more into the term Theme.

-> Theming with Design Tokens (Draft)

I know people use theme to describe their light or dark color-scheme, which is still possible for them to do. I think, the key is to separate this into theme and features. As this group aims to land a spec that targets enterprise size solution, the more range the definitions cover, the better.


⬇️ ⚠️ From here on, I expect you to have read the excerpt above.

3. Formalizing a Spec Draft

When formalizing this, it is critical to define goals and non-goals.

Goals:

  • The spec is more like a schema to a database, as the token file is a storage of structured data
  • The spec is to provide a structure to in which information can be stored
  • The spec is providing all the information for tools to understand the data much like humans
  • The spec is human friendly and allows manipulation in text-editors
  • The spec allows for validating the data to ensure data integrity

Non-Goals:

  • The spec is not based or constraint by implementation behavior
  • The spec does not assume or prefer specific working mode (tool vs manual, context or global editing)

With that in mind, I worked on a draft. I'll have to make assumptions on the way (that also target @kevinmpowell's questions).

1. Define the Capabilities of a Theme

Assumption 1: One folder with multiple token files contain a theme.

So far the spec defines the structure within one file. But I can't remember reading about multiple files (please send a link, if there is a mention). That is I assume it the way style-dictionary does: Multiple files in one directory, then all files are merged into one in-memory "database" which is then translated into desired formats.

For that a theme first must mention its capabilities in terms of features and scopes. I'm using typescript here to model this:

interface Theme {
  name: string;
  features?: Features;
  scopes?: string[];
}

interface Features {
  [feature: string]: {
    default: string; // must be available in options
    options: string[];
  }
}

... and an example to it:

{
  "name": "ocean",
  "features": {
    "color-scheme": {
      "default": "light",
      "options": ["light", "dark", "midnight"]
    },
    "color-contrast": {
      "default": "low",
      "options": ["low", "high"]
    }
  },
  "scopes": ["invert"]
}

This information can be read, interpreted and understood by tools and humans. When authoring the theme, these are the constraints.

2. Multiple Token Values

I'd follow the approach for multiple token values, but take in the idea from @ddamato about context oriented editing and evolve from there.

// intents.tokens
{
  "intent-action-base-background": {
    "$type": "color",
    "$value": "darkblue"
  }
}

and another file for dark:

// intents-dark.tokens
{
  "intent-action-base-background": {
    "$type": "color",
    "$value": {
      "$value": "lightblue",
      "color-scheme": "dark"
    }
  }
}

The $value definition gets more detailed by describing the constraints under which this value is valid.

Next up, bring in a scope:

// scope-invert.tokens
{
  "intent-action-base-background": {
    "$type": "color",
    "$value": {
      "$value": "lightblue",
      "$scope": "invert"
    }
  }
}

The same token defined three times, in three different files. They can be compiled into one file:

// compiled.tokens
{
  "intent-action-base-background": {
    "$type": "color",
    "$value": [
      {
        "$value": "darkblue"
      }, 
      {
        "$value": "lightblue",
        "color-scheme": "dark"
      }, 
      {
        "$value": "lightblue",
        "$scope": "invert"
      }
    ]
  }
}

... and both formats are valid and acceptable. Apparently, when all of the files above are merged, the compiled one is the output.

Let's turn this into a formalized definition, using typescript again (simplified of course):

/* 
this is the way how a current value can be defined, depending on its type, etc. 
As this is not relevant here, I assigned it to a string - but give it a name to 
make the connection to the work that was already put into here
*/
type CurrentValue = string;

interface ConstrainedValue {
  $value: CurrentValue;
  $scope?: string; // must be the values defined in meta file
  [feature: string]: string; // must be the values defined in meta file
}

type SingleValue =
  | CurrentValue
  | ConstrainedValue;

type Value = 
  | SingleValue
  | SingleValue[];

This will continue to use special fields prefixed with $ and features use user-defined values. That rule makes it possible to validate the token files.

Validating the Spec

Ok, so here is a good way to validate the spec against some test-cases and whether it confirms ot not.

  1. Is the spec coherent? yes, capabilities can be defined in the meta file

  2. Does the spec allow people to define their situations? yes, capabilities can take any user-defined values

  3. Does the spec allow validation of token files? yes, as token values are defined based on the user-defined values and the structure accounts for them. Validation routines can be written by tooling authors

  4. Does the spec prefer or assume a specific working mode? no, it can be used in a single file or multiple files and authors can define their own organization

  5. Is the spec human friendly? yes, especially as it allows authors to define their own organization of files with their prefered working mode.
    no, it may be cumbersome to always define the constraints when working in one file that always uses the same constraint (see open question below)

  6. Is the spec tool friendly? yes, thanks to the meta file. Which contains the information about the capabilities. That's the required information for tools to generate UIs based on these information.
    At the same time allows tooling authors to write theme-switchers.
    It also allows to take a collection of *.token files and wrap them in a custom build scripts for filtering or put a REST API around them as demonstrated in the article.

  7. Is the spec coupled to a particular implementation? no, the spec defines a structure to organize data into information.

  8. Is the spec coupled to environment implementation details - such as media queries for color schemes? no, that's up to authors to define their feature options and implementation to connect this with the given technical environment.

  9. But there are given technical constraints for color-scheme and color-contrast, how does the spec connect here? I see it similar to how the $type defines the structure of $value. It potentially started with only primitive values (string, number, boolean) into what border-radius needs and by that enhancing the spec with more of such use-cases. I can see the same evolution happening here, in which the spec enhances such types. I would even want to see this, but at first is to create the boundaries and frame the picture in which this can happen.

  10. You like that meta file very much, is that a solution to everything? 😎 haha, no. I'm also not working for meta. I needed a name to refer to that, can also be a theme config 😁

Open Questions

There are a couple of open questions

Q: How to find the "meta" file?

Is there a special name meta.tokens or does each file reference the meta file?

{
  "$reference": "path/to/meta.tokens"
}

Probably, a question asked too early?

Q: What's the conflict resolution algorithm when merging files?

As the spec allows for validating the files against the capabilties set in the meta file, when merging files (especially through tooling) they can validate the files and log error messages, such as:

  • Found two values for the same token: xx-yy-zz
  • Missing token value for dark color-scheme and low contrast for: another-token
  • ...

I think, that's already the case for style-dictionary. Tools can provide configuration for severity levels (warn vs error).

Q: Do I always have to set the constraints on each token value? Can't I set it for the entire file?

I can already hear the people scream about this 😇 That's a question for constraining an entire group and not part of this issue. I've seen other discussion around this, please link them.

Q: Do features need to be $features in ConstrainedValue?

With features being next to $value and $scope they eat up all space for future enhancements (unless they come $-prefixed). Which takes away real-estate within the object. Instead they can be fenced in its own object:

intents-dark.tokens
{
  "intent-action-base-background": {
    "$type": "color",
    "$value": {
      "$value": "lightblue",
      "$features": {
        "color-scheme": "dark"
      }
    }
  }
}

that is making it more explicit for sure, but also a bit more cluttering. Tools wouldn't care, but reduces the "human-friendly" part of it.

I don't have an answer but wanted to state I was discussing this situation.


My own opinions on this: I took the comments of this issue matched them with my own research and come up with this draft. I think, it answers a lot of questions. It is despite quite concise and I even managed to keep it human friendly (I hope). I'm somewhat surprise about the outcome here (now that I wrote up all this). Super interested in hearing your feedback.

gossi avatar Apr 14 '23 13:04 gossi