design-tokens / community-group

This is the official DTCG repository for the design tokens specification.
https://tr.designtokens.org
Other
1.49k stars 60 forks source link

[RFC] Theming #2

Open kaelig opened 5 years ago

kaelig commented 5 years ago

As all platforms are just about to support their own versions of dark mode, theming is becoming an important part of an efficient design workflow.

Let's talk about how design tokens should handle theming.

Here's a rough draft that essentially mimics how the CSS cascade works:

// Defaults
[
  { name: 'tokenA', value: 'foo' },
  { name: 'tokenB', value: 'bar' },
]

// Overrides for dark mode, loaded subsequently
[
  { name: 'tokenA', value: 'baz' },
]

// Should resolve to:
[
  { name: 'tokenA', value: 'baz' },
  { name: 'tokenB', value: 'bar' },
]

In a design tool, a designer could load and toggle any number of override files in order to get to the target platform (such as: Defaults < Android < Dark mode).

mathieudutour commented 5 years ago

a designer could load and toggle any number of override files

That's one way to think about it: one tokens file per theme, very much like what Theme UI is doing.

Another way would be to have tokens having a value keyed by theme. And so when you define a token, it's only defined in a single place and it's super easy to reason about.

I feel like the first approach makes more sense as an API to pass to an app or a component while the second one is how designers think about tokens and would make a lot more sense in design tools.

mkeftz commented 5 years ago

I like the idea of combining the tokens to produce a single tokenlist. Probably most of all because this could leave the managing of variants outside of the spec!

However, I think there is an issue with how they are combined. Being based on the order they are loaded will give different results based on how the designer loads them. Load Dark, then Android could give a different result to Android, then Dark.

I feel like the first approach makes more sense as an API to pass to an app or a component while the second one is how designers think about tokens and would make a lot more sense in design tools.

In my opinion, a design tool could handle either format and display it in a way that makes sense for the designer. But I do agree the "value keyed by theme" option makes it more human readable.

The issues I have with "value keyed by theme":

These could be fixed by extending from https://github.com/design-tokens/working-group/issues/1#issuecomment-504607402

{
  // define all variant dimensions and possible values
  variants: [
    { name: "theme", values: ["light", "dark"] },
    { name: "platform", values: ["web", "ios", "android"]  }
  ],

  tokens: [

    // simple if you don't need theming 
    { name: "token2", value: 3 },

    // only specify values for the variants you want to override, 
    // all others get default "value"
    { 
        name: 'tokenB', 
        value: 'baz',
        variants: [
            {
                theme: 'dark',
                value: 'foo'
            }
        ]
    },

    // can define combined variant values, i.e. Dark and Android
    { 
        name: 'tokenA', 
        value: 'baz',
        variants: [
            {
                theme: 'dark',
                platform: 'android',
                value: 'bar'
            }
        ]
    },
  ]
}

@mathieudutour - you mentioned you found it safer to provide a value for all variants. What makes it safer? I believe having a base value and only overriding what you need makes it safer and easier to manage. The reason I find this approach safer is if you miss a variant value, you just get the default. Otherwise, you would get a "Not defined" error.

Kilian commented 5 years ago

@mkeftz your approach is more forgiving, so 'safer' in that regard, but @mathieudutour is more explicit, meaning that any bugs caused by referencing something that's not there will be made explicit too. In the context of theming (and tokens in general), does having a fallback make sense? I can think of arguments for both sides so I'm not sure.

I prefer the cascade option with multiple files. Then there's the option of having both dark.tokens and android.tokens overwrites but you can also have a android+dark.tokens override should you need it.

One thing the spec could do is make it easy to cross-reference between these files, so that the android+dark.tokens could do something like this pseudocode:

{
   colors: 'dark.tokens/@colors',
   spacing: 'android.tokens/@spacing',
}

other values like typography would then just live in the default.tokens file loaded previously.

mathieudutour commented 5 years ago

I feel like tokens will need to be split up in different files anyway, but I was imagining it more split by "type" (eg. color, typography, etc.) instead of theme.

So I'm not sure the matrix of variants would really happen: I have a really hard time imagining a token that actually need to be split across a matrix. It's either per "theme" (colors) or per "platform" (typography) but not really both, right?

Kilian commented 5 years ago

A .tokens file could be a container, making that split possible.

Saying that there is never an overlap between variants is very opinionated and in the case of eg responsive design, colors might be different depending on the ‘size theme’ that could then interfere with a dark mode theme. Or think of a high contrast theme that needs to work with multiple platforms but also itself changes sizings for readability.

So I would stay away from thinking of themes as ‘just different platforms and dark mode’ and instead treat them all as equal variants and think of ways they should(n’t) be able to interact.

On 26 Jun 2019, at 10:20, Mathieu Dutour notifications@github.com wrote:

I feel like tokens will need to be split up in different files anyway, but I was imagining it more split by "type" (eg. color, typography, etc.) instead of theme.

So I'm not sure the matrix of variants would really happen: I have a really hard time imagining a token that actually need to be split across a matrix. It's either per "theme" (colors) or per "platform" (typography) but not really both, right?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

mkeftz commented 5 years ago

So I'm not sure the matrix of variants would really happen

I disagree... sure not for simple themes like "Dark mode". But branding themes are often more complex. For example, take a look at Seek's Braid DS. Here is one of their token files: https://github.com/seek-oss/braid-design-system/blob/master/lib/themes/jobStreet/tokens.ts They define font size, space, etc. in the Theme token file. They don't support platform-specific variants yet AFAIK, but if they did add it, it could also contain space tokens.

mathieudutour commented 5 years ago

Indeed, that's a good point. Thinking a bit more about it, I quite like @mkeftz's proposition here: https://github.com/design-tokens/working-group/issues/2#issuecomment-505754003.

But as mentioned in the other thread (https://github.com/design-tokens/working-group/issues/1#issuecomment-504611074), while I understand the appeal for a default value + overrides for variants (convenience, conciseness), I think that specifying all the variants makes it more explicit, safer, and makes sure there is only one way to write your file (hence, a lot easier to parse, etc.). If someone is worried about an combinatorial explosion, they can split their tokens in different files so that only the ones that needs to interact with different variants defines them.

dbanksdesign commented 5 years ago

I do like @mkeftz's proposal as well. I will say we should really focus on the interoperability aspect, and with that being flexible and extensible. There were some comments on usability from a standpoint of writing these, but the tools (Sketch, Figma, Theo, Style Dictionary, etc.) could make those usable abstractions and output the underlying token spec. Does that make sense? So some tools could allow you to write tokens like @mkeftz's proposal, but maybe the underlying token spec is more simple and flexible? (Although, the proposal is pretty simple and flexible already)

kgcreative commented 5 years ago

Hi all! -- no specific company representation here beyond experience and an interest in this topic.

Two things. First,

Regarding variants, I think a way of specifying inheritance can help resolve some of these questions. This would both make it specific, and help resolve collisions.

For example (pseudocode, building off of Kaelig's example)

// Defaults
defaults [
  { name: 'tokenA', value: 'foo' },
  { name: 'tokenB', value: 'bar' },
  { name: 'tokenC', value: 'baz' },
]

// Overrides for Brand A
brandVariantA [
 { name: 'tokenA', value: 'qux' }
]

// Overrides for dark mode
darkMode[
  { name: 'tokenC', value: 'xyz' },
]

// You can then specify a custom variant that specifies order of inheritance
myVariant [
  { name: 'inherits', value: 'defaults' },
  { name: 'inherits', value: 'brandVariantA' },
  { name: 'inherits', value: 'darkMode' },
  { name: 'tokenD', value: 'abc' }
]

// Should resolve to:
[
  { name: 'tokenA', value: 'qux' },
  { name: 'tokenB', value: 'bar' },
  { name: 'tokenC', value: 'xyz' } ,
  { name: 'tokenD', value: 'abc' }
]

And second,

To chime in on branding themes and overrides, a specific use case I don't see mentioned when specificity is discussed is what happens when the variant is a relative value. This is fairly common for me when dealing with responsive design, but not as much with native.

For example, when i'm working with responsive typography, often I want type on a breakpoint to be relative to the base type. This is especially useful if i'm ceding the user control with things like zoom, and I want my units and spacing to stay proportionally the same.

Similarly for color, sometimes I want a value that's darker or lighter than a base hue, but I want the base hue inherited. Again, something fairly trivial to do in code, but endlessly complex in design tools.

This has in the past been a source of frustration with design tools, because I can specify 1.13em on web, but there is no way to do that in my design tool without deriving a computed value and locking it in place.

Same with color. I can use tint/shade functions to darken or lighten a hue on web, inheriting a particular value, but in other contexts, i need to derive a computed value in order to simulate it for my design tool.

It means if I change my mind about the base font size, for example, my design tool doesn't reflect the change in real time. I have to manually edit all my styles.

c1rrus commented 5 years ago

Excellent points. The lack of support for relative length units like em or rem in many design tools irks me too.

Thinking of a standardised token format, I'd suggest it should support such units (or at least some equivalent thereof).

Perhaps if a tool that reads token files does not support relative units internally, it should convert to a unit it does understand (e.g. by substituting 16px for 1rem) as part of the parsing process. It's a lossy conversion, but for many uses it should be acceptable.

The question becomes what the behaviour should be when such a tool subsequently writes out any updates or additions to the token file?

Perhaps a sensible compromise would be:

If an author wishes to convert those, they need to do that themselves afterwards. Either manually or perhaps with the help of some other tool. At least this way they only need to do so once.

ddamato commented 4 years ago

One of the approaches that I use within CSS is to have a proxy in between the root variables (think a color palette) and the parts of the component to which they are applied (think applying a value to a CSS property).

:root {
    --color-red--500: #e51c23;
    --color-red--600: #dd191d;
}

[theme="brand"] {
    --backgroundColor-buttonCritical--pressed: var(--color-red--500);
}

[theme="wild"] {
    --backgroundColor-buttonCritical--pressed: var(--color-red--600);
}

.my-danger-button:active {
    background-color: var(--backgroundColor-buttonCritical--pressed);
}

In this way, the selection of the token to use is done by determining the intent of the color instead of the specific value. When selecting a theme, the new proxy is loaded in, reading from the available palette. If a decision is made to change all values of critical button backgrounds in a particular theme, it's done without finding all the instances.

While this is probably less of an issue within design tools where components are created as reusable symbols and their values are assigned at the design system maintainer level. I can expect newer components not yet included in the system to require some guidance in determining what color (or any token for that matter) to use instead of just picking any red so that it aligns with a provided system or theme.

NateBaldwinDesign commented 4 years ago

Hi there 👋 ! I'm particularly interested in the issue of theming and token authoring. This is one problem that is seemingly simple but can become incredibly complex. There are a few thoughts that I have regarding this issue:

  1. Definition of "Theme"
  2. Abstracting contexts for token flexibility
  3. Philosophical use of overrides
  4. Token definition vs. tooling

Definition of "Theme" I believe some clear definitions may be helpful prior to having deeper discussion into theming. For some folks, "theme" refers to the color palette used. For others, a "theme" refers to the entirety of the design language itself (think parent companies that have vastly different brands using the same core components).

There's value in recognizing that for each type of token a user defines (color, spacing, typography, etc), there is the possibility of having relative "themes"; essentially just different contexts that can be applied to a design language (as mentioned by @mkeftz regarding complexity of brand 'themes' above).

I believe the original comments of this thread assume "theme" to relate to "color themes". If that is the case, I would recommend using that language with the assumption that there may also be "scale themes", or "typographic themes" which designers may combine into unique design languages.

The rest of my comments are under the umbrella of "color themes"

Abstracting contexts for token flexibility What @ddamato illustrates above is exactly where I believe there's value in approaching the issue of themes. There should be abstractions in tokens that allow for global context (such as color palettes), relative context (such as semantically aliasing tokens to common uses), shared contexts (such as common groups of tokens that are represented in multiple different components) and explicit context (such as defining a button's tokens for a button only). Although the shared contexts layer is not always needed depending on implementation.

Global context (at least for Adobe's design system Spectrum) is where users define the hard values that they wish to use. This has been tremendously helpful in allowing us to combine color theme and scale independently. Users can define only the color values for each theme in separate files, thus allowing contextual and explicit aliases to only be written once. ie:

// light.js
   'gray-100': 'rgb(245, 245, 245)';

// dark.js
   'gray-100': 'rgb(50, 50, 50)';

// colorAliases.js
   'background-color-default': 'gray-100'

// sideNavigation.js
   'background-color: 'background-color-default'

Explicitness in errors (such as "Dark.js does not have token 'gray-100'") is fine in my opinion. It helps designers to ensure there's parity in the applied philosophy behind their design system, even though it is bound to them in some ways. Although this can be mitigated. For example, if we don't have gray-100 in dark.js, you should be able to specify in the alias layer what you would like instead. For example:

// colorAliases.js
   'background-color-default': {
        'light': 'gray-100',
        'dark': 'some-other-color'
    }

Philosophical use of overrides Regarding the discussion on overrides for themes, it comes down to the question of what is a default for any given thing? The idea of a color palette having a "default value" is very unusual and will result in very bizarre outputs that will create confusion for designers -- especially if some themes have colors that are very similar to the "default" (design may overlook or not even see that there is an error).

Overriding makes sense when there is a sensible default, such as default states of a component. The notion of overrides in terms of defining state-related color changes, or variant-related color changes is 100% appropriate.

But when it comes to global-level tokens, I'm in the camp of throwing an error. Theming (for both color and all other types of theming) can become very complex, and "defaulting" to a particular value could be more problematic than just throwing an error. For example, Brand A has a theme that is overridden by Brand B. Brand A is vibrant in color, and Brand B is a more muted B2B theme. If a color token defaults to the vibrant Brand A color rather than throwing an error, the result would be highly undesirable.

Token definitions vs Tooling I believe token standardization can happen agnostic of tooling. Tools can be built to do any number of things, and the standard should not be predisposed to assumptions about problems a tool may have to solve. Relativity in tokens is a must-have in my opinion. Many platforms support relative values, and their use in a design system is tremendously beneficial. Many (if not most/all) design decisions made by systems designers revolve around relativity... so even when a designer says "give this item 32px height", sometimes what they're really saying is "this item has the same height as the base type line height".

If tokens can be defined as relationships (wherever needed), that will help designers to codify the gestalt of their design aesthetic into every detail of their system in a much more manageable way.

ddamato commented 4 years ago

In terms of "defaults", I like to be prescriptive for the theme. Even if we have a primary brand theme, it is not based on default values, it's based on values specific for the theme. I think the "default" is there for when a theme fails. In the case of the web, perhaps it's better to have a default in to ensure the page is readable as the theme loads in.

.my-danger-button:active {
    background-color: var(--backgroundColor-buttonCritical--pressed, '#f00');
}

The theme for the components may have failed in loading, so we fallback to the #f00 which isn't part of any "theme", it's the default value. It's certainly possible that no change is detected; as in --backgroundColor-buttonCritical--pressed's value could be #f00.

In the case of overriding, I think that's less of a design token issue and more of a permissions issue. Design tokens are shared values for a managed design system; if you don't want to use those values, then you are simply deciding on different values. However, if the tool's permissions say you must use these tokens only (with some leeway in approved places), then I think that's a separate topic. I think what's being asked for here is a method of accessing predetermined values in design tools as well as custom values.

applinist commented 4 years ago

Hi everyone 👋
I would like to share with you how we implemented theme management within our design tool, Toolabs. The theory behind our approach is explained in detail by Jon Gold in his article Declarative Design Tools. A simplified version of Toolabs DSM is also available as a plugin for AdobeXD, you may want to have a look at it to see our theme management in action.

{
  // define all variant dimensions and possible values
  states: [
    {
        id: "FV5ynCbAL",
        name: "media-size",
        variations: [
            "small",
            "medium",
            "large"
        ],
        default: "large"
    },
    {
        id: "H5cn-A1b9",
        name: "color",
        variations: [
            "light",
            "dark"
        ],
        default: "“light"
    },
    {
        id: "nYdoI4p1O",
        name: "contrast",
        variations: [
            "low",
            "high"
        ],
    },
    {
        id: "be7agXfOD",
        name: "language",
        variations: [
            "English",
            "German", 
            "French"
        ],
        default: "English"
    }
  ],

  tokens: [
    {
        name: 'tokenA',
        value: 'baz',
        variants: [
            {
                color: 'dark',
                value: 'foo'
            },
            {
                color: 'dark',
                contrast: 'high',
                value: 'bar'
            }
        ]
    }
  ]
}

This definition generates a permutation of [3 media-size] x [2 color theme] x [2 contrast] x [3 language] = 36 state variations.

Overrides for token values might be made for any of these variations. However, to process token values for any given state, instead of fallback to the default value on absence of override, our approach is to traverse the array of ordered permutation of variations to find the last override value for that token.

For the given example, the processed value for tokenA for state color_dark + contrast_low is “foo”, not “baz”. In order to find this value, we use the permutation of variations for current state to generate the token value matrices :

State Variations tokenA ... other tokens
default baz ...
color_light -- ...
color_dark foo ...
contrast_low -- ...
contrast_high -- ...
color_dark + contrast_low -- ...

On the other hand, for state color_dark + contrast_high, since we already have an override for this exact state, we end up with value "bar" :

State Variations tokenA ... other tokens
default baz ...
color_light -- ...
color_dark foo ...
contrast_low -- ...
contrast_high -- ...
color_dark + contrast_high bar ...

I am aware that a solution like this which depends on permutation of state variations might yield to huge and complex structures and therefore make it less human readable in a file format. Therefore, I am not sure whether this approach is appropriate for design-tokens spec to handle theming or not but wanted to share with you because I believe that when presented with a good UX by design tools, it provides designers a powerful way of managing complex cases beyond just color themes.

NateBaldwinDesign commented 4 years ago

@applinist I love this. That's in line with how we're thinking as well. Speaking to this as a "system state" is a nice way of consolidating those terms as they are a similar model. I agree with the notion of overrides as you mention them in your tables. I'm not sure I would qualify those as "overrides" in the cases where no token value is assigned for a particular state variation (ie, "contrast_low" has no value for tokenA, therefore it's not an "override").

I am of the opinion that systems authoring needs to support a model that allows for crazy-complex situations like what you're alluding to. Companies and brands have a variety of ways in which they want to strategize the implementation of a "theme", and conceptualizing this as "system states", which can be exponentially compounded is a flexible solution that will more-often-than-not show itself as simply a "light mode"/"dark mode" paradigm. It's simple enough that it doesn't overcomplicate this use case.

sh0ji commented 4 years ago

Hi all :wave:! I also favor @applinist's state matrix approach—it should be able to accommodate a huge range of possible user preferences.

It's worth pointing out that CSS Media Queries has done a lot of work thinking about color scheme and color contrast preference that should be of interest here. They also have an open issue about gradients of high contrast that might make us want to reconsider whether just the two contrast tokens (high/low) are enough (assuming the "variation" values are meant to be normative).

It would be awesome if setting a design token state matrix could apply relevant media queries at a level that's easy to override (the user agent, for instance) rather than asking authors do all the work themselves. Writing/generating the CSS required to implement this would be quite a burden. For example, here's one way to do this right now for just the light (scheme) + high (contrast) & dark + high color combinations:

:root {
    --color_scheme-light-contrast-high: #foo;
    --color_scheme-dark-contrast-high: #bar;
}

@media screen and (prefers-color-scheme: light) and (prefers-contrast: high) {
    .foo {
        color: var(--color_scheme-light-contrast-high);
    }
}

@media screen and (prefers-color-scheme: dark) and (prefers-contrast: high) {
    .foo {
        color: var(--color_scheme-dark-contrast-high);
    }
}
c1rrus commented 4 years ago

Really interesting ideas here!

I'm going to throw another idea into the mix: The ability to define "interfaces" (similar to object-oriented programming) for tokens.

My thinking is that some of the use-cases being discussed here could be addressed by having an interface that defines the "slots" (feel free to suggest a better name!) - i.e. things values are required for and what type those values should have.

You could then define a sort of abstract color scheme, or UI widget or complete UI theme as interfaces. Then there could be one or more implementations that fill them with values (e.g. "light" and "dark" color schemes, "pill button" and "primary button" UI widgets, "brand A", "brand B" and "brand C" UI themes, etc. etc.)

Here's some pseudo code to illustrate my idea. This example implies a few ideas being discussed over in issue #1:

{
  // Let's begin by defining a few "plain" design tokens.
  // This part is essentially the basic file format we're tying
  // to define over in #1:
  "tokens": {
    "brand-colors": {
      "radioactive-red": {
        "id": "color-rr",
        "value": "#ff0000",
        "type": "color"
      },
      "gross-green": {
        "id": "color-gg",
        "value": "#00ff00",
        "type": "color"
      },
      "brilliant-blue": {
        "id": "color-bb",
        "value": "#0000ff",
        "type": "color"
      },
      "demo-dupe": {
        "id": "color-dd",
        // References the value of the token
        // with ID "color-gg"
        "value": "@color-gg"
      }
    },
    "sizes": {
      "small": {
        "id": "size-xs",
        "value": "0.1rem",
        "type": "length"
      },
      "medium": {
        "id": "size-s",
        "value": "0.2rem",
        "type": "length"
      },
      "large": {
        "id": "size-m",
        "value": "0.4rem",
        "type": "length"
      }
    }

    // An interface could just be a special type of design token
    // whose value defines the names, types and optional
    // default values that make up that interface.
    "my-color-scheme": {
      "id": "mcs",
      "type": "interface",
      "value": {
        "foreground": {
          "type": "color"
        },
        "background": {
          "type": "color"
        },
        "accent": {
          "type": "color",
          "id": "page-cols-ln"
        },
        "accent-2": {
          "type": "color",
          // References a default value
          // from within this interface
          // using a special interface-relative
          // syntax
          "value": "@mcs/accent-2"
        },
        "danger": {
          "type": "color",
          // Reference an explicit token value
          "value": "@color-rr"
        }
      }
    },

    // Other "plain" design tokens (and also interfaces)
    // can now reference yet-to-be-defined values from within
    // an interface. This uses the same interface-relative
    // syntax shown above.
    "link-color": {
        "value": "@mcs/accent"
    },

    "button": {
        "fill": {
            "value": "@mcs/accent-2"
        },
        "label": {
            "value": "@mcs/foreground"
        }
    }

    // Interfaces can be implemented by other design tokens
    // by setting their type to the interface's ID reference.
    "page-color-theme-a": {
      "type": "@mcs",

      "value": {
        "foreground": {
          "value": "@color-bb"
        },
        "background": {
          "value": "#ffffff"
        },
        "accent": {
          "value": "@color-gg"
        }
        // accent-2 and danger
        // will resolve to the default
        // values specified in the interface
      }
    },

    "page-color-theme-b": {
      "type": "@mcs",

      "value": {
        "foreground": {
          "value": "@color-gg"
        },
        "background": {
          "value": "#000000"
        },
        "accent": {
          "value": "@color-bb"
        },
        "accent-2": {
          // Override default value from interface
          "value": "@color-rr"
        },
        "danger": {
          // Override default value from interface
          "value": "#cc0000"
        }
      }
    }
  }
}

"Plain" design tokens

There's no surprises in the first part, a tool like Theo or StyleDictionary could render out those "plain" design tokens to SASS, JS, XML or whatever. For example:

// SCSS output
$brand-colors-radioactive-red: #ff0000;
$brand-colors-gross-green: #00ff00;
$brand-colors-brilliant-blue: #0000ff;
$brand-colors-demo-dupe: #00ff00;
$sizes-small: 0.1rem;
// ...

Similarly, a visual design tool like Sketch might make all those color tokens available in its color picker, or show a dropdown of the available length tokens when setting things like font-sizes, dimensions, border radii, etc.

Interfaces

On to the new, exotic "interface" tokens. For many tools, these might be inert and not get exported or exposed in any way. However, I think a case could be made for something like Theo or StyleDictionary to export them as a TypeScript interface or something like that.

import { Color } from '@dtcg/types';

export interface PageColors {
  foreground: Color;
  background: Color;
  link: Color;
  linkVisited: Color;
  danger: Color;
}

Likewise you could imagine a GUI tool that displays a kind of form to the user with all of the slots an appropriate inputs for each (color pickers, font pickers, etc.) and/or dropdowns of compatible design tokens (based on their type) that can be assigned to each slot. Such a tool could then save the assignments back out to a design token file as a new implementation (see below).

References to interface values

Any tokens the reference an interface value are essentially inert until an implementation of that interface is chosen. So, a tool like StyleDictionary might simply ignore such tokens, when no implementation is chosen or available.

I suppose tools might use their own conventions to either auto-select one of the available implementations, or pre-fill some default values if none is available. Of course, if a tool can find a sensible use or representation of such "abstract" design tokens, then it is free to do so.

Implementing an interface

As shown in my example, an interface implementation is simply a special type of design token. My example contains 2 implementations for the color-scheme interface: page-color-theme-a and page-color-theme-b. So, let's imagine that a hypothetical future version of Theo has some kind of option for choosing an implementation and the user has chosen page-color-theme-a. Then it can output code for the abstract design tokens as follows (using SASS as an example):

$link-color: #00ff00;
$button-fill: #00ff00;
$button-label: #0000ff;

In this example, the value of $button-fill was resolved as follows:

Strong typing

The reason I think a strong notion of design token types is important in all of this, is that it allows tools to do some useful things for the users:

Multiple design token files

While my example above is one, big design token file, I would expect there to be some ability to divide things up across multiple files. The rules and syntax for that will be something we'll need to define as part of the basic design token file format (see #1).

Aside from letting teams split their code into smaller, more manageable chunks, this could provide an interesting opportunity: If we allow URL-based imports, so you can reference remote design token files, then teams could expose interfaces for others to implement in their design systems.

Imagine a 3rd party UI widget being able to expose a sort of public design token API for all its themeable properties via an interface. Design systems that want to use that widget could them import that interface and implement it within their design token files using their own design token values.

If I've understood the intent of System UI's theme specification correctly, that could potentially be converted into a design token interface as well. (And if I got that totally wrong, please do enlighten me!)

Relationship to System States

I'm undecided whether this is an alternative to @applinist's system states, or if they are complementary. My hunch is that they are complementary, but I'm unsure how exactly they might be combined. If I've understood system states correctly, there's an implication that all possible variants described by a set of states must be of the same design token type (e.g. a color, a length, a font name, etc.). So perhaps states could therefore be incorporated as a way of defining the permitted value variants that tokens of a particular type may have.

mathieudutour commented 4 years ago

While this is an interesting idea, I'd say it's out of scope: trying to define "molecules" as opposed to "atoms" (which is what tokens are)

c1rrus commented 4 years ago

I certainly agree that my interfaces proposal is out of scope for an initial version of our design token file format spec. However, since this thread is discussing possible approaches to support theming use-cases, I do think it's worth considering and debating that proposal as a possible approach to work towards in future spec versions.

Re-reading this thread and the other approaches being proposed, I wonder if people have slightly different mental models / approaches of how to use and organize design tokens.

So, to try and disambiguate, allow me to explain how I think of them. I'd be really interested to hear where others here have differing ideas / approaches / terminology.

I think most here will agree with the basic definition from SalesForce Lightning:

Design tokens [...] are named entities that store visual design attributes. We use them in place of hard-coded values (such as hex values for color or pixel values for spacing) in order to maintain a scalable and consistent visual system for UI development.

In my mind, a basic use of design tokens, with a few colors and lengths being directly applied to UI elements could be visualised like this:

image

In practice though, most design systems define some intermediate abstraction layer(s). These will often use more semantic names that try to describe the intent of the token. However, to keep things DRY, those may ultimately reference other tokens with explicit values.

I quite like the names Nathan Curtis uses for these 2 groups of design tokens: Options (the named values) and Choices (the aliases that represent the choice of using a particular value for some purpose).

Extending my little example with this concept, you might have something like this:

image

As systems grow in complexity, teams may introduce additional layers of abstraction within the choice design tokens. If I understood @NateBaldwinDesign's earlier comment correctly, this is what he was alluding to (and please do correct me if I misunderstood!). Using his terminology, I'd say global context is the similar to Nathan Curtis' options. Then, relative contexts, shared contexts and explicit contexts are perhaps sub-divisions of the choices.

So, extending my example with an extra layer of abstraction might yield something like this:

image

Based on what I've read and my own hands-on experience, I think the decisions around what breadth of choice design tokens to have, what to call them and how many layers of abstraction to have are all bespoke to each design system. Getting this right is hard and, I suspect, most design systems will need to iterate and evolve the relationships they model via their choice design tokens over time.

For that reason, I don't think anything the DTCG defines should prescribe a particular structure or naming convention to users. I see our job as defining a format that enables users to make token hierarchies as flat or deep as they like.

But, at the same time, what we come up with also needs to facilitate interoperability (otherwise, what's the point) so that tools can operate on arbitrary design token files in a meaningful way, or use such files to exchange design token data between them.

That's why I believe that references (or "aliases", if you prefer), typing for token values and as little constraints as possible on how tokens are organised within a design token file are important features. References are the means by which we enable people to express choices. Types are the means by which we let tools operate on arbitrary design token files (since they can differentiate between, say, a color value and a font name regardless of what those tokens are named or how they are categorised). Finally, being un-opinionated about how tokens are grouped or organised within a design token file (or perhaps even across multiple files) mean we don't constrain design system teams.

So that's how I see tokens and how I think people use them. Hopefully, the format we define over in issue #1 will support all of the above somehow.

If people here have different mental models or views on any of this, I'd love to hear them. Firstly, it's fascinating. Secondly, we will eventually need to form some kind of consensus around this.

Theming

Moving on to the topic of this thread, my understanding of the broader "theming" problem we want to solve is this: A design system may need a means to vary its token values.

There can be many reasons for these variations - and combinations of them may well be needed (which can result in a large number of possible permutations):

My reading of the initial approach @kaelig suggested at the start of this thread was to have a base file with your default design token values and then one or more override files that get layered on and alter the values of some tokens. Subsequent comments have refined this, but that basic idea remains.

I see my interface proposal as an alternative to that (although not necessarily an mutually exclusive one). My feeling is that defining explicit interfaces gives a means to authors to express which of their tokens are intended to have their values be tweaked (almost like a public API) and which are considered "internal" to that design system. That in turns could also enable tools to expose or manipulate those tokens in a helpful way.

For instance a design system may wish to enforce that links and buttons always have the same colour, but want that shared color to be something that is themeable.

Extending my example diagram once again, here's an illustration of the interface approach:

Design token interfaces (If my previous comment was confusing, hopefully this clears things up a bit!)

In this case I'm just showing a single interface with 2 possible implementations. However, you could define as many interfaces as you like. They can be as high or low in abstraction as you like and could be interconnected. In the interest of not being too opinionated, it would up to each design system to decide how many interfaces it wants to expose and at which level(s) of abstraction they are. Or indeed, if it wants to use interfaces at all. :-)

Coming back to the original theming challenge, I feel this approach does offer a potential solution:

So yeah, I feel this proposal is in scope for this discussion :-D

Whether or not y'all like is another matter of course. Please go ahead and scrutinise it thoroughly. I'm sure there are gaps and issues I haven't considered.

mathieudutour commented 4 years ago

I think what we discussed up til now is to have "themes" (or "variants" or whatever we want to call it) at an even lower level than tokens.

Then you have tokens that can depends on variants (and define different values depending on the variant value).

So we end up with a state machine with really clear outputs: the tokens have always the same interface (their id), and to resolve their value, you just have to set the value of each variant.

ezgif com-gif-maker (sorry for your eyes 😬)

If I understand correctly, instead of having variants apply to potentially all tokens, an "interface" would only apply to a fix set of tokens.

I can see how it'd be useful to only expose certain tokens (eg. show button-background which is the alias of red100 which would be "hidden/private"). But I'm struggling to see how "interfaces" solves anything, I feel it's too complex? Like its' trying to define the possible states of a state machine but also define some of its value?

You are right that the discussion about it is totally in scope tho, and that we have different mental models about tokens 😄 But I do think that "interfaces" as sets of tokens to expose is probably out of scope, because too application specific.

kevinSuttle commented 4 years ago

Has anyone thought about how this would be defined at the Search Engine level? In terms of JSON-LD?

https://github.com/schemaorg/schemaorg/issues/944

c1rrus commented 4 years ago

@mathieudutour

If I understand correctly, instead of having variants apply to potentially all tokens, an "interface" would only apply to a fix set of tokens

Yes, that was my thinking.

I'm struggling to see how "interfaces" solves anything, I feel it's too complex?

Quite likely. I have a tendency to overengineer things. ahem ☺️

Thanks for your explanation and GIF, I think I understand the state machine stuff much better now. (It's late where I am, so will need to re-read this and the earlier comments with fresh eyes tomorrow to make sure I've properly grokked it)

Could certain variants be constrained to tokens of a particular type? For instance, light and dark variants make sense for color values, but less so for a border radius.

In any case, I'm glad we're exposing the different ways of thinking about this stuff!

mathieudutour commented 4 years ago

Could certain variants be constrained to tokens of a particular type? For instance, light and dark variants make sense for color values, but less so for a border radius.

I don't think there is a need for it. You would just omit the overrides for that particular variant. There is an example here: https://github.com/design-tokens/community-group/issues/2#issuecomment-531986633

c1rrus commented 4 years ago

Has anyone thought about how this would be defined at the Search Engine level? In terms of JSON-LD?

schemaorg/schemaorg#944

Not that I'm aware of. Thanks for bringing that to our attention. Will need to take a closer look at that tomorrow (I love me some structured metadata!).

kaelig commented 4 years ago

@kevinSuttle thank you for sharing! Can you elaborate on the possible use cases?

GarthDB commented 4 years ago

@kevinSuttle (hi) using this as a base existing spec to extend with design tokens for a full design system spec?

tobestobs commented 4 years ago

This is a great discussion. I'd like to throw in a few more thoughts on the definition of "themes". In my experience the word is used to express different concepts so it's worth being specific.

A lot of the time theme is used to refer to brand expression (applying a color palette to UI). Dark Mode support across platforms adds a second dimension to this since a UI palette can now have two different manifestations in light and dark mode.

Then there is the wider concept of layouts responding to other conditions defined by device attributes (pixel density, screen size, inout/output mode, etc.), user context (attention level, motor skills, vision, etc.) and environment (location, time of day, etc.). This last group is probably better labeled "contexts" rather than themes but can impact the values of Tokens if supported.

I'm wondering if the brand expression / UI color use case maybe doesn't need a solution in tokens since that can be expressed in the values assigned to the tokens. For the other use cases you could see a "context" concept in tokens that could look something like

context { name = "brightness" description = "A context to toggle light/dark mode" options = ["light_mode", "dark_mode"] default = "light_mode" }

context { name = "legibility" description = "a11y context to make text easier to read" options = ["standard", "high"] default = "standard" }

The tokens could then reference this with something like (making up syntax here, not a proposal)

component.heading-1.color = dark_grey component.heading-1.color[brightness=dark_mode] = light_grey component.heading-1.color[legibility=high] = black component.heading-1.color[legibility=high, brightness=dark_mode] = white

NateBaldwinDesign commented 4 years ago

If people here have different mental models or views on any of this, I'd love to hear them. Firstly, it's fascinating. Secondly, we will eventually need to form some kind of consensus around this.

This is a very good point. And I'm very happy to see the diagrams and examples provided above to help shine light on other folk's mental models for this problem. So while this may not add tremendous value to the thread, I'd like to share the model that we've been working with regarding tokens and theming (in relation to the various ways theming has been referenced above).

At a basic level, we consider "theming" as the highest-level form of context/system states: image Global tokens (options) are defined in separate 'theme' files for each of the unique contexts. In these files, each token is assigned a value. So we have the concept of "Gray 700" that has multiple possible values based on color theme. Tokens are mapped to aliases (sometimes multiple aliasing layers) and then captured in what we've been calling mixins (which I believe is the same concept as "interfaces"). Here we gather a group of tokens and define the group. This allows us to map groups of tokens to various components, in this case a radio button. The component extends (and can override) any of the tokens.

In this basic example, it's clear that theming happens at the earliest stage of token authoring, and is only seen again in the outputs/implementations. No worrying "wait, what should we do here for dark theme?".

The second item that has been referenced above as possibly the same notion as "theming" is the idea of variants. The way we've been approaching this is to assume a variant to be an extension of a default (much like we consider the API for components themselves). There are defaults, and a variant extends all tokens and overrides any values authored within the variant file. image

So here we're looking at the same Radio button example with a new "Emphasis" variant. The variant inherits all of the same tokens, however it overrides specific color tokens (which are extended from another mixin).

Component states may be treated in the same regard as variants, except that it's very valuable to retain a close relationship with the component they are referring to. This is a simplified view of one component property (border color) related to states. image States, for us, are included in many of the mixins that we author -- that way, when we have a basic concept for a control, we can group all shared tokens for each state that we wish the base component to have. The states are inherited in the component, and can be overridden / extended within the variants as well.

It's also worth noting that the brief mentions of having a single token file is extremely worrisome to me, considering the amount of tokens we output for our system is in the ballpark of 44,500 token values. Each of the layers shown in my diagrams above are separate files in our system, which helps for management of the various abstractions. Whatever direction takes, especially if authoring and maintenance is required to be taken within the codebase, it should be modularized.

I have bias to certain aspects of our approach that have helped us out, but it has plenty of pitfalls as well. In particular, if you consider localization as a theme (which I think we should), that adds another layer of complexity, since it is not as simple as RTL support or different fonts. Considerations to typography and even component padding/spacing may be needed to optimize design per locale. If we consider semantic use of color, things get hairy. You may define a set of global colors, and then assign semantics to certain color (red = bad, green = good) however in Japan those colors meaning is inverted... so the "localization" theme would apply to a new global context (directionality - RTL / LTR), and to some global tokens (font), some alias tokens (semantics), and some component-specific tokens (padding). Other theming concepts can be easily centralized to a particular layer of abstraction, but this one is more complex. Needless to say, I do not have a good recommendation on this front, only to heed to its complexity and commonality with other concepts of "theming".

Anyway, hope this is helpful -- if even to help frame my previous, and any future comments as to my own mental model 😅.

mathieudutour commented 4 years ago

Am I wrong to think that "mixin", "interface", "component", and what you call here "variant" (which is completely different from how we defined it before) is pretty much just groups of tokens as defined here?

Then it's up to the user to be consistent about the groups they define but I don't think there is for the format to enforce any kind of concept which for me are really application specific.

For example your second example could be described with just "variants"/"states", "value", "aliases" and "groups" as such:

{
  // define the different possible states
  states: [{
    name: "theme",
    values: ["Light", "Dark"],
    default: "Light"
  }],
  tokens: [
    {
      name: "blue-500",
      type: "color",
      // default value of the token
      value: "#00F",
      variants: [
        {
          // override the value of the token when the theme is Dark
          theme: 'Dark',
          value: '#00A'
        }
      ]
    },
    {
      name: "gray-700",
      type: "color",
      value: "#AAA"
    },
    {
      name: "control-color",
      // reference another token
      value: "$gray-700"
    },
    {
      name: "emphasis-color",
      value: "$blue-500"
    },
    {
      name: "basic-control",
      // group some tokens
      value: [
        {
          name: "background-color",
          value: "$control-color"
        },
        {
          name: "border-color",
          value: "$control-color"
        },
      ]
    },
    {
      name: "enphasis-control",
      value: [
        {
          name: "background-color",
          value: "$emphasis-color"
        },
        {
          name: "border-color",
          value: "$emphasis-color"
        },
      ]
    },
    {
      name: "radio-button",
      value: "$basic-control"
    },
    {
      name: "radio-emphasis",
      value: "$enphasis-control"
    }
  ]
}

So in the mental model I have of tokens, interfaces/groups/mixin/components are not related to themes/variants/states.

I believe we should try to keep the "API" surface as small as possible, without introducing concepts that could certainly help maintain larger sets of tokens but are more "conventions".

ilikescience commented 2 years ago

As I dig through the discussions more and more, I'm seeing (just as you are, @mathieudutour) some convergence in the underlying intention of mixins, interfaces, components, and variants. Just like you said, they're groups of tokens.

{ "colors" : {
  "type": "group",
  "value": {
    "dark": {
      "type": "group",
      "value": {
        "foreground": {
          "type": "color",
          "value": "#ffffff"
        }, {
        "background": {
          "type": "color",
          "value": "#000000"
        }
      }
    }, {
    "light": {
      "type": "group",
      "value": {
        "foreground": {
          "type": "color",
          "value": "#000000"
        }, {
        "background": {
          "type": "color",
          "value": "#ffffff"
        }
      }
    }
  }
}

This allows for a lot of flexibility in parsing. If a user wants all their tokens to be output a single js file, a token might end up with the name colors.dark.foreground. But if a user wants the tokens to be split up into separate files by group name and rendered to css, it might give you a colors/dark.css file with the token named --foreground.

There's a second convergence around aliasing. In the example above, I have to write out #ffffff and #000000 twice each. Aliases make this much easier to maintain.

{ "colors" : {
  "type": "group",
  "value": {
    "base": {
      "type": "group",
      "value": {
        "white": {
          "type": "color",
          "value": "#ffffff"
        }, {
        "black": {
          "type": "color",
          "value": "#000000"
        }
      }
    },
    "dark": {
      "type": "group",
      "value": {
        "foreground": {
          "type": "alias",
          "value": "colors.base.white"
        }, {
        "background": {
          "type": "alias",
          "value": "colors.base.black"
        }
      }
    }, {
    "light": {
      "type": "group",
      "value": {
        "foreground": {
          "type": "alias",
          "value": "colors.base.black"
        }, {
        "background": {
          "type": "alias",
          "value": "colors.base.white"
        }
      }
    }
  }
}

I think the two concepts together neatly cover the use case of themeing in that they enable a lot of flexibility on the user side without sacrificing readability on the token file side. Additionally this approach keeps the grammar simple.

What do you y'all think?

drwpow commented 2 years ago

I’m excited to see this discussion picking back up! @ilikescience I really like your attention to aliasing and grouping theme-related tokens. I also really like your point about simple grammar as well.

Not to distract from your suggestion, but your example brings to mind a question I’ve had for a while: Do tokens belong to themes? or do themes belong to tokens? I think many of us think of themes as first-class citizens, but I’m just curious if there’s value in inverting that assumption. If tokens own themes, the schema would be concise and also allow fallbacks. Further, it can reduce the amount of refactoring to your design tokens whenever a theme is added/removed/changed (when adding a theme, you wouldn’t have to copy + paste all 100% of your tokens; you’d only have to modify the tokens that changed).

By keeping tokens as first-class citizens, rather than themes, it could reduce complexity (up for debate, of course!). For example:

{
  "colors": {
    "type": "group",
    "value": {
      "white": {
        "type": "color",
        "value": "#ffffff"
      },
      {
      "black": {
        "type": "color",
        "value": "#000000"
      },
      "foreground": {
        "type": "alias",
        "value": "colors.black",
        "theme": {
          "dark": "colors.white",
          "light": "colors.black" 
        }
      },
      "background": {
        "type": "alias",
        "value": "colors.white",
        "theme": {
          "dark": "colors.black",
          "light": "colors.white"
        }
      },
      "active": {
        "type": "color",
        "value": "#0000ff",
        "theme": {
          "dark": "#4080ff"
        }
      }
    }
  }
}

Note: this schema format is outdated, but just matching the current discussion.

In this example, theme could be a simple mapping because it borrows the type from its parent, saving you from having to redeclare it again (i.e. it’s the same format as "value"). Also, active only needs to declare a separate value for the “dark” theme because all other themes inherit the default value.


One last thought: theme might be too broad; I’ve heard the word mode used as a more granular term (example). For example, say you have light/dark mode, and you have “enlarged text“ mode (a11y). Rather than have all permutations of color themes + text size themes, you could call light/dark a “mode” for color, and text size a “mode” for type, allowing you to freely combine them without having to make a bajillion themes.

ilikescience commented 2 years ago

Tokens vs. themes as first-class citizens (or, first-order organizational paradigms?) is a really interesting question. In this case I'd suggest that themes are merely a way to use groups - in your example, you can simply further nest your tokens/groups.

So instead of:

      "active": {
        "type": "color",
        "value": "#0000ff",
        "theme": {
          "dark": "#4080ff"
        }
      }

You'd have

      "active": {
        "type": "color",
        "value": "#0000ff",
        "theme": {
          "type": "group",
          "value": {
            "dark": {
              "type": "color",
              "value:" "#4080ff"
            }
          }
        }

Whereas 'theme' isn't a special reserved word, it's just the name you chose for that group. So you'd have a nicely namespaced token named active-theme-dark or activeThemeDark or however you wanna parse the names.

I do think this raises some of the same concerns as we're discussing in #97, though - if you have a token and a group called 'active', it gets a bit confusing.

And in writing up this comment, it occurs to me that you'd probably want your tokens organized by theme when actually using them, rather than putting themes at the end of the taxonomical chain.

drwpow commented 2 years ago

Yes there’s definitely a lot of overlap with #97 for sure. And to your point, organizing by groups does let you use both tokens at once, which you may want. That flexibility is a good thing.

But there’s also times at which I not only want a guarantee that variants of a certain thing exist, but also want to restrict certain things from being used together. For example, if I have theme-dark-active, I need to also make sure that theme-light-active and theme-colorblind-active exist, and it would be a lot of work to keep all these in sync. And I want to also ensure those palettes are never used together (e.g. when in colorblind mode, enforce colorblind-friendly palettes). So for that reason I like to call it “modes,” which only apply to a group. A theme is therefore merely a collection of “modes.”

Again, I could do this with groups, but keeping all these themes synced is just a lot of work either manually or by building tooling. And I selfishly just want the schema to take care of that for me 😄

tounsoo commented 1 year ago

Very late to the party here, but I think we might be missing something here - appearance.

For example, when you are using Swift's colorset, it requires 2 color per set for dark and light appearances and they are not considered 2 separate themes. I believe the browser's preferred-color-scheme concept is also similar, even though they call it a theme.

I think we might need to define what theme means,

Sample Swift colorset

{
  "colors" : [
    {
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "0.200",
          "red" : "0x14",
          "blue" : "0x14",
          "green" : "0x14"
        }
      },
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "0.200",
          "red" : "0xff",
          "blue" : "0xff",
          "green" : "0xff"
        }
      },
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}
tounsoo commented 1 year ago

Not sure if this is a good idea but just throwing out there to brainstorm with y'all.

Using some of the format Swift color set uses, I made a sample that I think might work well with color tokens being expandable.

As per current state of naming, I didn't add $ in front of the luminosity and accessibility because in this example, those are names not a unique design token attributes.

{
     "gray": {
            "50": {
                "$value": "#FFFFFF",
                "$type": "color",
                "$appearance": {
                    "luminosity": {
                        "dark": {
                            "$value": "#000000"
                        }
                    },
                    "accessibility": {
                        "AAA": {
                            "$value": "#000000"
                        }
                    }
                } 
            }
      }
}

This concept is considering each theme to be a brand and will result in less files to manage:

- theme-1.tokens.json (brand1)
  - gray-50
    - default
    - dark
    - accessible
  - ...
- theme-2.tokens.json (brand 2)
  - gray-50
    - default
    - dark
    - accessible
  - ...

One of the alternative I could think of would be using folder:

- brand 1 (folder)
  - dark.tokens.json
    - gray-50
    - ...
  - light.tokens.json
    - gray-50
    - ...
  - accessible.tokens.json
    - gray-50
    - ...
- brand 2 (folder)
  - dark.tokens.json
    - gray-50
    - ...
  - light.tokens.json
    - gray-50
    - ...
  - accessible.tokens.json
    - gray-50
    - ...