Repository navigation
Token schema #1410
Replies: 8 comments 1 reply
|
Thanks for writing this up, @NateBaldwinDesign. The core problem you're pointing at is real. Right now, a token as a design concept has no canonical identifier. The only thing tying the light, dark, and wireframe rows together is a shared Let me go through all four parts and then say where I think we should take it. 1. Merging per-mode duplicates into one object with a This is the big change, and I have three reservations worth putting on the table first. We have been here before. RFC #646 proposed a nested, grouped token shape, and during Phase 2 the spec deliberately moved to flat per-value objects after we hit real problems implementing the grouped version (the history is in The per-value UUID is also doing more work than it looks like. It is the target for The part I keep coming back to is the platform manifest. Our whole manifest model rests on layered cascade resolution: every value from every layer (foundation, platform, product) is an independent record that competes, and a platform adds records instead of editing foundation ones. Per-platform custom modes (the 2. The This is where your model is genuinely nicer. Expressing "light + high-contrast" as a single entry with a 3. Renaming Agreed, and it's overdue. The SDK and TUI already call this structure "classification" internally ( 4. Optional Also agreed. It's cheap and additive, and it solves the real gap where a future token with no Where I want to take this Instead of accepting or rejecting the structure in this thread, I want to compare three options properly and bring the findings back here:
That third row is the middle path I want to take seriously. Keep the per-value records and the cascade exactly as they are, and add a stable I've set up a spike to work through all three against the same sample tokens, covering manifest and custom-mode behavior, aliasing and diff identity, Figma sync, combinatory modes, and migration cost, and I'll post the full comparison back here. The |
|
Spike results are in. I ran all three shapes against the same sample tokens (a real multi-mode color alias, a dark plus high-contrast combinatory case, and a cross-token alias) and scored them on 13 criteria across two dimensions: platform manifest and cascade behavior, and identity plus migration cost. Full write-up with the worked samples is in proposal 013 in the repo. The short version: Manifest and cascade (6 criteria). valuesByMode comes out ahead on exactly one, the combinatory-mode syntax, and strained on the other five (overrides, extensions.tokens, modeSetRestrictions, extensions.modeSets, and cascade layer isolation). They all reduce to the same thing: collapsing the per-mode records into one concept object means a platform can no longer add or override a single mode's value as an independent record without reaching into a foundation-owned array. The current shape and the concept-id middle path behave identically here, and both stay clean. Identity and migration (7 criteria). valuesByMode wins exactly one, concept identity, which is the same and only criterion the concept-id middle path also wins. On the other six, concept-id keeps aliasing granularity, diff continuity, replacedBy, and Figma sync byte for byte identical to today, leaves the legacy setUuid and setSchema fields as removable as they are now instead of promoting them into the core structure, and costs one additive field instead of a schema, cascade, aliasing, and sync rewrite plus a rewrite of every data file. So across all 13 criteria, the only thing valuesByMode wins that the middle path doesn't is the combinatory-mode syntax. That's a real win and I still like it, but it's separable from the identity problem you raised, and it doesn't require the full restructure. Here's what that looks like concretely. Take {
"name": {
"colorRole": "accent",
"property": "color",
"state": ["default"],
"colorScheme": "light",
"legacyKey": "accent-background-color-default",
"object": "background"
},
"$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json",
"$ref": "90d82778-1cbb-47c0-aab9-b6e38a9cdc54",
"uuid": "d9d8488d-9b38-47e0-9660-dcad040f3ca8",
"set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e",
"set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json"
}The middle path leaves every field alone and adds one: {
"name": { "...": "..." },
"$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json",
"$ref": "90d82778-1cbb-47c0-aab9-b6e38a9cdc54",
"uuid": "d9d8488d-9b38-47e0-9660-dcad040f3ca8",
"conceptId": "accent-background-color-default",
"set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e",
"set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json"
}The dark and wireframe rows get the same For the combinatory case (dark plus high-contrast), the current shape needs a new row with both fields set in the name object: {
"name": {
"colorRole": "accent",
"property": "color",
"state": ["default"],
"colorScheme": "dark",
"contrast": "high",
"legacyKey": "accent-background-color-default",
"object": "background"
},
"$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json",
"$ref": "<high-contrast-dark-target>",
"uuid": "<new-uuid>",
"conceptId": "accent-background-color-default"
}valuesByMode handles it more directly, as an entry inside the existing concept object: {
"modes": [
{ "colorScheme": "dark", "set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e" },
{ "contrast": "high", "set_uuid": "<high-contrast-set-uuid>" }
],
"$ref": "<high-contrast-dark-target>"
}That's the one place your shape is genuinely nicer than mine. Where I land:
Happy to walk through the proposal 013 write-up if any of the per-criterion calls are worth digging into. |
|
Thank you for taking a look at this @GarthDB. I'm certain there's a tremendous amount of detail and context I'm missing, so the proposal is really high-level concept rather than explicit recommendation. I think that adding the I see the benefit of having I'm also curious about this:
If that's the case, are there guardrails to ensure there is no cross-modal referencing? For example, we have Let me show a real world example:
I worry that if people can cross-referentially map to independent modes for their values, the design aspect of the system could become fragile. For that, I'd wonder if there's an alternative that ensures you can have the mode-specific value level of diff'ing and history without the potential for disintegrating a conceptual framework like system-level contexts (modes)? As for combinatory mode values, maybe we can create an example to work with? There's some simple examples for text colors we could devise even just to see how the current setup works. For example, this would be directionally close to what we might come up with in the future:
|
| Light value | Dark value | |
|---|---|---|
| Regular contrast | accent-color-900 |
accent-color-800 |
| Low contrast | accent-color-800 |
accent-color-700 |
| High contrast | accent-color-1000 |
accent-color-900 |
|
Shared my comment before seeing that you already replied - reviewing your latest comment now, I see you have a combinatory mode example 👍 |
|
Thanks, @NateBaldwinDesign. A few of these are new questions, so let me take them one at a time. On not just using the token's uuid: the per-value uuid already carries a lot of weight, it's the diff unit, the On abstracting it into a separate object (Figma-variable style) and the workflow mapping: that full separation is basically what On the guardrail itself: you're right that the abstract model doesn't stop it. SPEC-001/SPEC-002/SPEC-003 check that a But when I went and checked the actual corpus, it's already working the way you want. Zero cross-mode references across all 710 aliases, and it's not close; none of them target a per-mode leaf value at all. They target either a set (resolved against the alias's own context) or a mode-agnostic anchor. Your own celery-background-color-default example turns out to be the set case, and I pulled the actual objects to make that concrete: {
"name": { "colorFamily": "celery", "state": ["default"], "colorScheme": "light", "legacyKey": "celery-background-color-default", "object": "background" },
"$ref": "b908fb61-f581-4942-9d00-57a828a0660b",
"uuid": "d4fd682d-4bef-4a92-bf14-90ce02b534e6",
"set_uuid": "5bd340a5-45fc-4666-82b2-827f9f17d6e4"
}{
"name": { "colorFamily": "celery", "state": ["default"], "colorScheme": "dark", "legacyKey": "celery-background-color-default", "object": "background" },
"$ref": "363508bf-9c3a-4e8e-9865-2b4915d2f097",
"uuid": "a9ab7a59-9cab-47fb-876d-6f0af93dc5df",
"set_uuid": "5bd340a5-45fc-4666-82b2-827f9f17d6e4"
}Light One catch: The real gap is that this safety currently rides on One more thing on the combinatory table you posted: I worked your exact 2x3 into Sample D in doc 013, all three shapes side by side. Only new wrinkle: Your guardrail push sharpened the recommendation rather than changing it: the SPEC-059 backstop and normativizing context-aware resolution as |
|
This sounds good, although I'm still unclear on the specifics and resolution to my concern of cross mode referencing (dark mode value of an alias referencing the UUID of the light mode value of another token). Adding that My top concerns are:
So long as the current direction fulfills these, then I'm happy. The appearance of redundant data is another concern I have but may not have functional implications or impact design directly, so I'll lean on your recommendation considering all other scenarios you're working towards that I may not be not aware of or fully understand. |
|
Awesome, this sounds good and thank you for the additional clarification. Since there are already tickets for the remaining work that would resolve anything that was not already demonstrated to be supported, I'll close out this RFC/Discussion. |
|
@NateBaldwinDesign this landed, so here's where things stand on your three points:
Everything's merged: #1428 has the conceptId work plus the resolution rule and SPEC-059, #1426 updates proposal 013, #1427 is the version bump. |
Uh oh!
There was an error while loading. Please reload this page.
Data model for multi-modal values
Context:
Design tokens represent a single idea, design concept, or use case. These use cases persist across a variety of current and future modes, such as
light,dark,lowcontrast,regularcontrast, andhighcontrast. Generally speaking a token has one value, but the value is based on conditions (such as mode or mode combinations).For example, blue-100 is a single "idea" about color, which has a value of
rgb(245, 249, 255)in light mode andrgb(14, 23, 63)in dark mode. Other tokens may support additional, combinatory modes such as "light + high contrast" or "dark + low contrast".The problem:
Today in
design-data, the data model for tokens follows the idea that every value has a unique data object and UUID. Effectively this means that a single token is repeated in the data for as many times as it has values, each time with a unique identifier.What that means is that a token, or design use case such as "The default, accent background color" has no singular identifier. There is no way to clearly identify this concept as a single token, because it's implemented three times:
{ "name": { "colorRole": "accent", "property": "color", "state": ["default"], "colorScheme": "light", "legacyKey": "accent-background-color-default", "object": "background" }, "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", "$ref": "90d82778-1cbb-47c0-aab9-b6e38a9cdc54", "uuid": "d9d8488d-9b38-47e0-9660-dcad040f3ca8", "set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e", "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json" }, { "name": { "colorRole": "accent", "property": "color", "state": ["default"], "colorScheme": "dark", "legacyKey": "accent-background-color-default", "object": "background" }, "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", "$ref": "87a2c8f0-54fd-4939-8f42-3124fde1e49e", "uuid": "f24eb871-6419-4cef-88a2-cca8548ae31e", "set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e", "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json" }, { "name": { "colorRole": "accent", "property": "color", "state": ["default"], "colorScheme": "wireframe", "legacyKey": "accent-background-color-default", "object": "background" }, "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", "$ref": "90d82778-1cbb-47c0-aab9-b6e38a9cdc54", "uuid": "1f4f6c48-633c-4eb5-b7d6-bf5a9a7fde18", "set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e", "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json" }Proposed solution
In order to model the data in a way that aligns to the conceptual ideas and methods of design tokens, I propose that we alter this approach so that:
What this might look like instead:
{ "uuid": "d9d8488d-9b38-47e0-9660-dcad040f3ca8", "displayName": "Accent background color default", "classification": { "colorRole": "accent", "property": "color", "state": ["default"], "legacyKey": "accent-background-color-default", "object": "background" }, "valuesByMode": [ { "modes": [ { "colorScheme": "light", "set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e", "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json" } ], "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", "$ref": "90d82778-1cbb-47c0-aab9-b6e38a9cdc54" }, { "modes": [ { "colorScheme": "dark", "set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e", "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json" } ], "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", "$ref": "87a2c8f0-54fd-4939-8f42-3124fde1e49e" }, { "modes": [ { "colorScheme": "wireframe", "set_uuid": "e05251ac-d64a-4157-9b20-224f0392269e", "set_schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/color-set.json" } ], "$schema": "https://opensource.adobe.com/spectrum-design-data/schemas/token-types/alias.json", "$ref": "90d82778-1cbb-47c0-aab9-b6e38a9cdc544" } ] }There are several changes in this proposal, so I will begin with the items that actually support and strengthen the schema and its mirroring of the mental model of tokens for design.
Merging all three tokens into one
Rather than 3 unique "tokens" (unique identifiers with highly duplicative data), the key differentiators are:
Because of that, there's no need for redundancies. Combining it into a single token with a single UUID makes referencing this data simpler.
The
valuesByModearrayThis could be an array but it could be an object too. What's important is that each object within the
valuesByModefield have the following information:modesarray, which can house multiple child objects (combinatory modes)colorScheme,set_uuid, andset_schemaso that this information is closer together$schemaand$refare at the same level within a single valuesByMode object, as any mode (or combination of modes) could result in a design value that is either analiasor a hardcoded value.How this could scale
Here's an example of how combinatory modes could be captured within this model of the JSON
There are less-important enhancements that I've included in this proposal. They don't particularly impact the challenge of mode-based values, but I felt they were worth including. These are described below.
Changing
nametoclassificationSince we are no longer using string names for tokens, and they're broken apart into cleaner object structures, this model directly represents that of a classification system (our formal taxonomy approach). None of this data is a "name" per se, despite it being used to generate names. The term "classification" for this object seems more fitting.
Adding a
displayNamefieldHumans will particularly refer to tokens by a name of some sort. If tokens emerge in the future that do not have a
legacyKey, then it's difficult to infer from the data itself what a particular token is called (theclassificationobject may not necessarily be ordered in the way they're referred to). Adding this field as optional seems like a nice way to surface a tokens' "name" without the overhead of platform-specific formatting or the nuance of how platforms may abbreviate or alter naming. This is a way for tools and humans to quickly identify what a token is in a human-friendly way.All reactions