Re: [community-group] Native modes and theming support (#210)

I love this thread, as it really demonstrates the complexity of something that, at the surface, seems intuitive or simple.

Couple of thoughts, and a proposal:

## 1. There are times where someone will care about and want to use multiple themes at the same time.

Which is to say, it isn't always true that someone works with one theme at a time. A few cases that demonstrate this:
a. One part of a page/view uses the "dark" theme, and a different part uses the "light" theme (see below)
b. One part of a page/view uses both a "dark" theme, and a "high density" theme, and a "brand X" theme.

For example, at Stripe, we have a few parts of our dashboard that we want to always show using the "dark" theme (some of our developer tools), regardless of the user-selected theme. Likewise, we have parts that always need to be in the "light" theme (previews of branded emails and invoices). That means, on one page, there might be a part of the page which has the user-selected theme, part that is always in a "dark" theme, and part that is always in a "light" theme.

The multi-file way of authoring themes might be convenient when the number of tokens/themes is high, but I think it would be a bit restrictive if it was the only way. We should come up with a robust way of authoring themes in the same file that can be extended to multi-file authoring.

## 2. Theme metadata like features, tags, priorities, and theme types may be over-complicating the spec

While the proposals by @gossi and @ddamato are very thoughtful and well-articulated, I worry that building too many new features into the spec is shifting complicated functionality from platforms up into the tokens format and into token translation tools themselves.

How interfaces are styled based on contexts — user-selected theme, accessibility preferences, localization, etc — varies widely from platform to platform. CSS, JS, Swift, Objective-C, Java, and Kotlin each have their own capabilities, quirks, and best practices for contextual styling. I think we should leave the complexity of understanding user contexts and applying styles to the UI to those platforms.

The token spec can (and I think, should) provide a high-level map that can be used by the platforms, but to make interoperability easy, it should be as lightweight as possible.

---

## Proposal: let's define themes with the least modifications to the current spec

Starting with the minimum valid token file:

```json
{
  "color-brand": {
    "$value": "#ff0000",
    "$type": "color"
  }
}
```
We’ll make only one change to the spec:

We introduce a "$themes" key.[^1] The value of $themes should be an object consisting of key-value pairs. the keys should be the name of the theme. The value should be either:
A tokens object
A string containing a path to a valid tokens.json file

So, a token file with themes will look like this:

```json
{
  "color-brand": {
    "$value": "#ff0000",
    "$type": "color"
  },
  "$themes": {
    "dark": {
      "$description": "the dark theme",
      "color-brand": {
        "$value": "#0000ff",
        "$type": "color"
      }
    }
  }
}
```

A token translator can apply the theme by merging the values in the $themes object with the "main" json object. Let's look at some more realistic examples:

### Example: a dark theme and light theme with token aliases in a single file

```json
{
  "colors": {
    "white": {
      "$type": "color",
      "$value": "#ffffff"
    },
    "black": {
      "$type": "color",
      "$value": "#000000"
    }
  },
  "$themes": {
    "light": {
      "foreground": {
        "$value": "{color.black}"
      },
      "background": {
        "$value": "{color.white}"
      }
    },
    "dark": {
      "foreground": {
        "$value": "{color.white}"
      },
      "background": {
        "$value": "{color.black}"
      }
    }
  }
}
```

Merging the light theme object with the main tokens object, we get the resulting json:

```json
{
  "colors": {
    "white": {
      "$type": "color",
      "$value": "#ffffff"
    },
    "black": {
      "$type": "color",
      "$value": "#000000"
    },
    "foreground": {
      "$value": "{color.black}"
    },
    "background": {
      "$value": "{color.white}"
    }
  }
}
```

Which is a valid, parsable tokens object! The translation tool is free to do with this as it pleases.

### Example: the same themes with multiple files

`tokens.json`
```json
{
  "colors": {
    "white": {
      "$type": "color",
      "$value": "#ffffff"
    },
    "black": {
      "$type": "color",
      "$value": "#000000"
    }
  },
  "$themes": {
    "light": "path/to/lightTheme.tokens",
    "dark": "path/to/darkTheme.tokens"
  }
}
```

`path/to/lightTheme.json`
```json
{
  "foreground": {
    "$value": "{color.black}"
  },
  "background": {
    "$value": "{color.white}"
  }
}
```

`path/to/darkTheme.json`
```json
{
  "foreground": {
    "$value": "{color.white}"
  },
  "background": {
    "$value": "{color.black}"
  }
}
```

The token translator could follow the path to the tokens file, and do the merge as before. This would result in the same outcome as above.

### Example: creating a “patch” 

There may be cases in which a user doesn’t want to output full theme. Perhaps they only want a “patch”, which includes only the tokens in the theme file, fully resolved.

Specifying a patch as an output, we could imagine a translator only resolving the token aliases and outputting the tokens specified in the theme. Using our tokens specified above, that might look like:

`Light theme patch`
```json
{
  "colors": {
    "foreground": {
      "$type": "color",
      "$value": "#ffffff",
    },
    "background": {
      "$type": "color",
      "$value": "#000000"
    }
  }
}
```

And again, the token translator could parse this into a useful output. This might be something that is put in a media query, where you don’t want to re-define every token.

### Example: stacking themes

As other folks have mentioned, you may want to apply multiple themes at a time. This could be done by the translation tool, in the order specified by the user at compilation time.

In this example, I’ll apply a dark mode theme on top of a brand theme.

Here’s the tokens file:

```json
{
  "colors": {
    "blue": {
      "200": {
        "$type": "color",
        "$value": "#75D5E8"
      },
      "700": {
        "$type": "color",
        "$value": "#04438C"
      }
    },
    "red": {
      "200": {
        "$type": "color",
        "$value": "#75D5E8"
      },
      "700": {
        "$type": "color",
        "$value": "#890D37"
      }
    }
  },
  "$themes": {
    "blueBarracuda": {
      "color": {
        "brand": {
          "200": {
            "$value": "{color.blue.200}"
          },
          "700": {
            "$value": "{color.blue.700}"
          }
        }
      }
    },
    "redJaguar": {
      "color": {
        "brand": {
          "200": {
            "$value": "{color.red.200}"
          },
          "700": {
            "$value": "{color.red.700}"
          }
        }
      }
    },
    "light": {
      "button": {
        "foreground": {
          "$value": "{color.brand.200}"
        },
        "background": {
          "$value": "{color.brand.700}"
        }
      }
    },
    "dark": {
      "button": {
        "foreground": {
          "$value": "{color.brand.700}"
        },
        "background": {
          "$value": "{color.brand.200}"
        }
      }
    }
  }
}
```

If we apply the `blueBaracuda` theme, and then the `dark` theme, we get the following output:

```json
{
  "colors": {
    "blue": {
      "200": {
        "$type": "color",
        "$value": "#75D5E8"
      },
      "700": {
        "$type": "color",
        "$value": "#04438C"
      }
    },
    "red": {
      "200": {
        "$type": "color",
        "$value": "#75D5E8"
      },
      "700": {
        "$type": "color",
        "$value": "#890D37"
      }
    },
    "brand": {
      "200": {
        "$value": "{color.blue.200}"
      },
      "700": {
        "$value": "{color.blue.700}"
      }
    }
  },
  "button": {
    "foreground": {
      "$value": "{color.brand.700}"
    },
    "background": {
      "$value": "{color.brand.200}"
    }
  }
}
```

The bonus to this approach is that I do not need to define separate dark and light themes for each brand! The token aliasing takes care of that for me. In this way, I think we can avoid the combinatorial explosion of multiple composable themes.

### Example: defining themes on the token level

I believe this approach works regardless of if you define a theme at the "base" level, or if you define it at a group level, or even a token level.

```json
{
  "colors": {
    "blue": {
      "$type": "color",
      "$value": "#75D5E8",
      "$themes": {
        "dark": {
          "$value": "#04438C"
        }
      }
    }
  }
}
```

Applying a theme simply requires merging the selected theme up into the token above it. In this case, applying the dark theme yields:

```json
{
  "colors": {
    "blue": {
      "$type": "color",
      "$value": "#04438C",
    }
  }
}
```

And parsing can continue from there.

## The downside to this approach

Because we’re not specifying the order that themes are applied within the tokens file itself, analyzing the file will not tell you if a theme’s aliases will resolve or not. For instance, if I took my brand-and-color-theme example above and only applied the dark theme (skipping the brand theme), the final output would have an alias to a {color.brand.200} token that does not exist. However, this can be caught and handled like any missing reference, and wouldn’t need special treatment just because it’s a theme.

I think that the tradeoff of flexibility, composability, and simplicity is worth the possibility of this kind of error in the transation pipeline.

## Ok, so how do translators know how to handle contexts?

We can offload the complexity of specifying which theme goes with which context to the translator.

Styledictionary, for example, separates out the tokens definition from the configuration for processing the tokens. Therefore, we could expect to have the themes defined as above, and have the instructions for processing the themes done however instructions are provided to the translator (a configuration file, flags on a command line script, etc): theme A should be translated into a `(prefers-color-scheme: dark)` media query, theme B should be put into a separate .css file, etc.

This gets at an as-of-yet-unexplored conversation about how much a .tokens file should be providing documentation of tokens vs. providing instructions to a translation tool. I expect a hardy and spirited discussion to follow :)


[^1]: this doesn’t have to be “themes.” It could be “modes,” too. I actually would prefer “contexts” to  these, as it is is both more general and thus more applicable. 


-- 
GitHub Notification of comment by ilikescience
Please view or discuss this issue at https://github.com/design-tokens/community-group/issues/210#issuecomment-1511701681 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Monday, 17 April 2023 16:26:41 UTC