Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pre-defined composite type for refering color modifications (e.g. rgba) #88

Open
markuslangl opened this issue Dec 17, 2021 · 13 comments
Open
Labels
dtcg-color All issues related to the color module specification. dtcg-format All issues related to the format specification. Needs Feedback/Review Open for feedback from the community, and may require a review from editors.

Comments

@markuslangl
Copy link

markuslangl commented Dec 17, 2021

Hi,

Our Design and Development teams are currently trying to implement a new Design system based on the specified document as of 2021-12-13 (with the current proposal of limiting composite types (#86) in mind). The whole standard is awesome, and we'd like to thank everyone involved for all the great work done!

Problem definition

We're currently struggling to create a correct colors.tokens.json with colors that use references parts of other colors, e.g. the RGB value or the opacity.
Our Design system consists of multiple levels of color definitions which reference to each other to provide consistency, but also long-term flexibility.

For example:

{
  "base": {
    "red": {
      "type": "color",
      "value": "#ff0000"
    },
    "green": {
      "type": "color",
      "value": "#00ff00"
    },
    "blue": {
      "type": "color",
      "value": "#0000ff"
    },
    "yellow": {
      "type": "color",
      "value": "#ffff00"
    },
    "background": {
      "type": "color",
      "value": "#ffffff"
    },
    "foreground": {
      "type": "color",
      "value": "#000000"
    }
  },
  // ...
}

Now, in order to define a component which is based on these variables, would use references to these values:

{
  // ...
  "components": {
    "alert": {
      "error": {
        "background": {
          "type": "color",
          "value": "{base.red}"
        },
        "foreground": {
          "type": "color",
          "value": "{base.foreground}"
        }
      },
      // ...
    },
  },
  // ...
}

But in our Design system, we'd like to reference the base color and modify it (e.g. add adding opacity to the value):

{
  // ...
    "alert": {
      "modify": {
        "background": {
          "opacity": {
            "value": 0.1
          }
        },
        "border": {
          "opacity": {
            "value": 0.3
          },
        }
      },
      "error": {
        "background": {
          "type": "color",
          // Invalid value
          "value": "rgba({base.red}, {components.alert.modify.background.opacity})"
        },
        "border": {
          "type": "border",
          "value": {
            // Invalid value
            "color": "rgba({base.red}, {components.alert.modify.border.opacity})",
            "width": "{border.base.m}",
            "style": "{border.base.style}"
          },
        }
      }
    }
  // ...
}

Unfortunately, this would result in an invalid tokens file: Currently only colors in the formats #RRGGBB and #RRGGBBAA are allowed at a value field when it has been defined as type color.

A potential workaround would be to use the calculated #RGBA value and add the reference to the extensions field.

For us, keeping the references in a standardised way which will be supported throughout code and design is important. A custom composite type would cover this use case as well, but this part will not be part of the specification MVP.

Concerns

We're concerned that not covering this use-case early would cause fragmentation in the ecosystem over time. If there's no common style for modifying the alpha value (or maybe even hue, saturation or lightness as example), there might be plugins and converters requiring different formats, which make it hard to create an easy-to-write file that works everywhere.

Example: Fictional workflow with Figma, Style Dictionary and Storybook

(Just to clarify, this workflow is fictional - all formats and decisions listed here are just exemplary)

The designers are creating their designs with Figma tokens, which might in a future version for example use their own extension com.figma.tokens:

{
  "background": {
    "type": "color",
    "value": "{color.base.red}",
    "extensions": {
      "com.figma.tokens": {
        // Note the opacity written as percentage (10% = "10") in this example
        "opacity": 10
      }
    }
  }
}

Another tool might use it's own extension or format to store the modification. For example, the documentation of style-dictionary's transitive transforms lists the following format in the examples (to provide compability with chroma-js):

{
  "background": {
    "type": "color",
    "value": "{color.base.red}",
    "modify": [{
      "type": "alpha",
      "amount": 0.1
    }]
  }
}

Generating the documentation from the tokens file in a tool like Storybook might get hard at this point, because there could be multiple sources of truth for opacities. If there was for example a generic <DesignTokens> renderer, how could it know how to correctly display the defined color?

As a workaround, it would always be possible to write converters between these tools, formats, and extensions. Having the format for these standardised would greatly benefit interoperability for users.

Benefits of referenced opacity values in tokens files

Our main motivation for referencing colors or modifications is to retain the references in CSS variables. We'd like to use the design tokens to create CSS variables directly from the token files, for example with this result:

/* base.css */
:root {
  --color-base-red: #f00;
  --color-base-green: #0f0;
  --color-base-blue: #00f;
  --color-base-yellow: #ff0;
  --color-base-background: #fff;
  --color-base-foreground: #000;

  /* Additional RGB values could be added by the CSS generator to support usage in rgba() */
  --color-base-red-rgb: 255, 0, 0;
  /* ... */

  --color-alert-background-opacity: 0.1;
  --color-alert-border-opacity: 0.3;
  --color-alert-error-background:
    rgba(var(--color-base-red-rgb), var(--color-alert-background-opacity));
  --color-alert-error-border:
    var(--border-base-m) var(--border-base-style) rgba(var(--color-base-red-rgb), var(--color-alert-border-opacity));
}

/* components/alert.css */
.alert-component--error {
  background: var(--color-alert-error-background);
  border: var(--color-alert-error-border);
}

We would benefit from using CSS variables instead of using the direct values once we enable other themes (e.g. a "dark" or "high contrast" version). It wouldn't be required to re-generate the whole file, but just to replace the some of the base variables in e.g. another dark.tokens.json file:

{
  "base": {
    "red": {
      "type": "color",
      "value": "#200000"
    },
    "green": {
      "type": "color",
      "value": "#002000"
    }
    // ...
  },
  "components": {
    "alert": {
      "modify": {
        "background": {
          "opacity": {
            "value": 0.2
          }
        },
        "border": {
          "opacity": {
            "value": 0.4
          }
        }
      }
    }
  }
}

Since the references in the second file would still be intact, it could generate a second file for (prefers-color-scheme: dark), which could only overwrite the new values and still work:

/* dark.css */
:root {
  --color-base-red: #200000;
  --color-base-green: #002000;

  --color-alert-background-opacity: 0.2;
  --color-alert-border-opacity: 0.4;
}

Proposal: Additional pre-defined composite type

We would like to propose adding basic color modifications to the MVP. This could help avoiding the need for parsing different color syntaxes (#79) and prepare for a future implementation of computed token values (#81).

We have no preferred syntax for this, but assume that an additional pre-defined composite type for "modified colors" would fit in best with the current state of the specification. This could also enable other composite types to use color or computed-color as values.

One potential format could be for example:

{
  "background": {
    "type": "modified-color",
    "value": {
      "color": "{color.base.red}",
      "modify": {
        "opacity": 0.1,
      }
    }
  }
}

This might eventually also enable further modifications to the color, like e.g.:

  • Hue
  • Saturation
  • Lightness
  • Red
  • Green
  • Blue
  • Tint
  • Shade
@donnavitan
Copy link

I really like this! I'm wondering if something like modified-color would be enough to address the examples noted at the end of your message. Or if we might need a few.

Similar to what you described, when I, as a designer, asked our dev's to include different opacities as design token options so that we can modify base colours. I was told that there wouldn't be an easy way to do this.

An option was to provide all colour tokens with siblings that have the opacity defined, but that would just create a whole bunch of colour tokens that might not be necessary at the time and confuse designers/devs.

In Figma, colour styles also have their opacity baked in. So, it seemed like an easy and quick solution at the time.

However, thinking of future use and scale, this is definitely something we should think about. Thank you for sharing!

@markuslangl
Copy link
Author

Thank you very much for your feedback!

I'm wondering if something like modified-color would be enough to address the examples noted at the end of your message. Or if we might need a few.

That's a great point! It might become probably a bit confusing what the expected result is, if users add e.g. hue and green at the same time to modify a color.

Maybe it could make the implementation easier, if there were several color composite types available, which limit the usage of the modifications? For example:

Composite type Allowed keys in value object
color-rgba color*, red or red-relative, green or green-relative, blue or blue-relative, alpha or alpha-relative
color-hsla color*, hue or hue-relative, saturation or saturation-relative, lightness or lightness-relative, alpha or alpha-relative
color-hsba color*, hue or hue-relative, saturation or saturation-relative, brightness or brightness-relative, alpha or alpha-relative
color-fulness color*, tint or tint-relative, shade or shade-relative, tone or tone-relative, alpha or alpha-relative

(* declares that a key is a required value for the type)

With this scheme, it might also be possible to avoid the modify wrapper to have less markup for the same result:

{
  "background": {
    "type": "color-rgba",
    "value": {
      "color": "{color.base.red}",
      "alpha": 0.1,
    }
  }
}

This could also become very powerful when these values could appear on all places where colors are defined, like e.g. the border composite type:

{
  "alert": {
    "opacity": {
      "background": {
        "value": 0.1
      },
      "border": {
        "value": 0.3
      },
    },
    "danger": {
      "base": {
        "type": "color",
        "value": "{color.base.red}"
      },
      "background": {
        "type": "color-rgba",
        "value": {
          "color": "{components.alert.danger.base}",
          // Set opacity/alpha to 10%
          "alpha": "{components.alert.opacity.background}",
          // Reduce red by 10%
          "red-relative": -0.1,
        }
      },
      "border": {
        "type": "border",
        "value": {
          // Using composite value instead of #RGBA
          "color": {
            "type": "color-hsla",
            "value": {
              "color": "{components.alert.danger.base}",
              // Set opacity/alpha to 30%
              "alpha": "{components.alert.opacity.border}",
              // Increase saturation by 50%
              "saturation-relative": 0.5,
            }
          },
          "width": "{border.base.m}",
          "style": "{border.base.style}"
        }
      }
    },
  }
}

Advantages and Disadvantages

The advantages with this versions could be:

  • The available options are limited to the specific color space and avoid conflicting states
  • Relative and absolute values can be used at the same time
  • References can be used for all values, not just the color itself
  • New color spaces could be added in future spec versions easily without modifications to the existing types
  • These types could be easier discovered than modified-color when using auto-complete (e.g. if users type col|, they can see color, color-rgba, color-hsla, etc.)

Disadvantages are:

  • It might become more work for vendors to implement the different types
  • Modifications to colorfulness (tinting, shading, toning) are currently not supported natively by CSS and would require processors like e.g. SASS or postcss-color-mod

Open question: Value scale

One open question would be, which units and scale would be most intuitive for relative and absolute values? A few options come to mind:

  1. Consistent percentages on a scale from 0 to 100?
  2. Consistent percentages on a scale from 0 to 1?
  3. Consistent percentages based on given unit (e.g. number between 0 and 1 or string with percent sign for value between "0%" and "100%")?
  4. Consistent values on a scale from 0 to 255 (to match the hex scale)?
  5. Different for each of the values as it makes more sense (e.g. red in percentage from 0 to 100, hue in degrees from 0 to 360, ...)?

Open question: Nested color composites

The second open question would be, is if the color type should be able to always be replaced with these new Color composite types, even inside the types themselves?

For example, should the following example result in text-color value of #00FFFF7F?

{
  "text-color": {
    "type": "color-rgba",
    "value": {
      "color": {
        "type": "color-hsla",
        "value": {
          "color": "#ff0000",
          "hue": 180
        }
      },
      "alpha": 0.5
    }
  }
}

Unfortunately, this has potential for wrong inputs when non-color types would be added to the color value:

{
  // Example with invalid content
  "invalid-text-color": {
    "type": "color-rgba",
    "value": {
      "color": {
        "type": "font",
        "value": "Fira Sans"
      },
      "alpha": 0.5
    }
  }
}

In our opinion, it would be very powerful and flexible for designers to define colors like this. We could define some base colors and generate most other colors in the tokens based on these.

If parsers would ignore invalid values such as "type": "font", or show error messages when creating or reading files, the benefits might be worth the potential for errors.

when I, as a designer, asked our dev's to include different opacities as design token options so that we can modify base colours. I was told that there wouldn't be an easy way to do this.

We're mainly seeing this from the perspective of Web development, but would love these definitions to be interoperable with all other platforms.

Are there different usages and definitions for other platforms like iOS or Android, that would limit the functionality of these color composite types?

@TravisSpomer
Copy link

TravisSpomer commented Jan 5, 2022

The token JSON format that we're using at Microsoft has two uses of computed colors:

Take a base color and overwrite its alpha value

{ "Hover": { "computed": { "color": "Global.Color.AccentBase", "opacity": 0.05 } } }

That's virtually identical to the solutions proposed above.

Take a base color and generate multiple tokens from it

{ "Color": { "Grey": { "generate": { "type": "lightness0to100by2", "value": "#808080" } } } }

Both of those are pretty important for our scenarios. Our core color palette has dozens of base colors and each one of them has about ten lightness variations, and we want those variations to be expressed programmatically. For a few colors, like grey, we use far more variations. Without some way to express that programmatically, we would need to have a preprocessor that would generate a W3C token file from our "enhanced" token file: a Sass or TypeScript for tokens, effectively. (It's not critical to us right now that arbitrary design tools like Figma be able to perform the color ramp generation from that latter case—just that we can express that in a valid token file.)

@jeromefarnum
Copy link

jeromefarnum commented Feb 4, 2022

At EA our color tokens support either providing a string value for the standard use case, an object for adjusting opacity, or an object for calculated colors.

Opacity adjustments like this:

{
  “some-color”: {
    “type”: “color”,
    “value”: {
      “color”: “{colors.another-color}”,
      “alpha”: 0.85
    }
  }
}

Though we don’t support it in our token system, this approach could be used in conjunction with transitive transforms so any aliased relationships are maintained through token output, like so in CSS:

—some-color: rgba(—colors-another-color, 0.85);

Calculated colors look something like this:

{
  “some-color”: {
    “type”: “color”,
    “value”: {
      “color”: “{colors.another-color}”,
      “operations”: [
        { “hue”: -20 },
        { “lightness”: 5 },
        { “saturation”: 13 }
      ]
    }
  }
}

The calculated colors operations property accepts a custom token type of color-operations so we can tokenize the operation set (basically they behave as curried functions) that we can reuse throughout our token themes.

{
  “custom-color-operation”: {
    “type”: “color-operation”,
    “value”: [
      { “hue”: -20 },
      { “lightness”: 5 },
      { “saturation”: 13 }
    ]
  },
  “some-color”: {
    “type”: “color”,
    “value”: {
      “color”: “{colors.another-color}”,
      “operations”: “{custom-color-operation}”
    }
  }
}

Unlike the alpha adjustments, I don’t think color operations can carry through transitively to token output.

@kaelig kaelig added dtcg-color All issues related to the color module specification. dtcg-format All issues related to the format specification. Needs Feedback/Review Open for feedback from the community, and may require a review from editors. labels Mar 8, 2022
@c1rrus
Copy link
Member

c1rrus commented Mar 20, 2022

Lots of really interesting suggestions here. Thanks everyone for sharing!

I'm not sure if additional types for modified/generated colors is the way to go though. My concern is that might make things more complex downstream if other things that expect color tokens want to reference those tokens. For example, if you wanted to use them in composite tokens like stops on a gradient or the color of a border. Granted, we could update the spec for those types to allow their sub-values to be color, color-rgba, color-hsla, etc. but that doesn't feel very maintainable.

Also, thinking ahead a bit, I can imagine other types besides colors also benefitting from some mechanism to manipulate their values. For example dimension tokens being calculated as multiples of other dimension tokens (maybe to generate a spacing scale or something like that). I'm wondering if we can come up with a syntax and mechanism that works for colors today but could be extended to other types too.

I therefore really like the approach @jeromefarnum describes, where you can have an array of operations. Also, being able to factor them out as an operations token seems pretty neat - though that might raise interesting questions (or possibilities?) if you have a token of type operations that itself has operations. Maybe the operations should get concatenated, effectively allowing you to chain them together.

Let's imagine we could do something like this:

{
  "some-token": {
    "$value": "#ff0000",
    "$type": "color",

    // list of operations that can modify
    // the color value. They are applied in
    // the order listed here
    "$operations": [
       {
         "hue": -20
       },
       {
         "alpha": 0.3
       } 
    ]
  }
}

When a token has $operations, its $value becomes the input to those operations. The token's resolved value is whatever the calculated value is after the operations have been applied. If other tokens reference this token, they should get the resolved value.

E.g.:

{
  "some-token": {
    "$value": "#ff0000", // input value
    "$type": "color",

    // list of operations that can modify
    // the color value. They are applied in
    // the order listed here
    "$operations": [
       {
         "alpha": 0.5
       }
    ]
    // resolved value is #ff00007f
  },

  "alias-token": {
    "$value": "{some-token}"
    // value is #ff00007f
  }
}

Whatever operations we define in our spec would need to be type-specific. The color type might have operations like hue, lighten, darken, alpha, etc., the dimension type might have operations like add, multiply, etc. and some types might have no available operations at all.

The rule would be that only compatible operations can be used. E.g. if your token is of type dimension and you use one fo the color operations, that would be considered invalid and tools should reject that token (and probably report an error to the user).

However, each operation can have parameters and can defined what types those parameters need to be. Furthermore, the parameters can be references to tokens of the required type.

Let's imagine we have a color operation called alpha and it takes a single parameter which is of type number. The number should be in the range 0 - 1 and will be set as the input color's alpha channel value (if numbers outside of that range are provided, they gets clamped to that range (so 999 would be treated as 1 for example)). The parameter could be a raw number value or a reference to a number token:

{
  "half": {
    "$value": 0.5
  },

  "some-token": {
    "$value": "#ff0000", // input value
    "$type": "color",

    "$operations": [
       {
         "alpha": "{half}"
       }
    ]
    // resolved value is #ff00007f
  }
}

So, for each operation we include in the spec, we'd need to define:

  • It's name
  • Exactly what it does (e.g. there might be more than one way to lighten a color, so we'd need to specify the exact algorithm to ensure that all tools that support our format produce the same results)
  • What type(s) it can operate on
  • What parameter(s) it has and, for each paramater:
    • What type(s) that parameter needs to be

What do you think?

@markuslangl
Copy link
Author

I think this fits our usage of the tokens perfectly and will be especially powerful also when adding extensibility for other values, like dimensions. Thank you very much!

Just one question with regards to the usage of the $operations object: Can a whole operation optionally be defined and referenced as a token as well? This refers to the last example given by @jeromefarnum.

It would be great to be able to define not just the values of an operation in tokens, but also a whole operation itself. This would make it easy to modify or replace operations without the need to change the respective key(s) in all tokens that use these operations.

For example:

{
  // "Global" constants
  "constants": {
    "mute-transparency": 0.25,
    "mute-saturation": 0.15,
    "colorize-hue": 0.2
  },

  // "Global" operations
  "operations": {
    // Specific "mute" operations (e.g. by transparency or saturation)
    "mute-transparency": {
      "$type": "operation"
      "$value": [
        {
          "alpha": "{constants.mute-transparency}"
        }
      ]
    },
    "mute-saturation": {
      "$type": "operation"
      "$value": [
        {
          "saturation": "{constants.mute-saturation}"
        }
      ]
    },

    // "Default" mute
    "mute": {
      "$type": "operation"
      "$value": "{operations.mute-transparency}"
    },

    // Other operation with multiple definitions...
    "colorize": {
      "$type": "operation"
      "$value": [
        {
          "saturation": 1
        },
        {
          "hue": "{constants.colorize-hue}"
        }
      ]
    }
  },

  "colors": {
    "brand": {
      "primary": {
        "$value": "#ff0000",
        "$type": "color"
      }
    }
  },

  "some-token": {
    "$value": "{colors.brand.primary}",
    "$type": "color",
    "$operations": [
      "{operations.mute}", // Default mute = {alpha: 0.15}

      // Add specific operation
      {"invert": 1},

      "{operations.colorize}" // [{saturation: 1}, {hue: 0.2}]
    ]
  }
}

If we were to change the "default" mute in future to use the "saturation" mute, it could be as simple as changing the $value of operations.mute to use operations.mute-saturation:

 "mute": {
   "$type": "operation"
-  "$value": "{operations.mute-transparency}"
+  "$value": "{operations.mute-saturation}"
 },

@beauroberts
Copy link

Hello 👋 Thanks very much for all the hard work you've placed into this spec, and specifically to @c1rrus for sharing at the Into Design Systems conf earlier today!

I want to pick up on his comment above about generalising the approach to handle transformation of any type of value, not just colours.

I think that would be amazing, and would support how we're currently working. In our internal token graph representation, we support intermediate transform nodes between token references in much the same way that is being proposed here, although our syntax is a bit...verbose:

{
  "opacity": {
    "half": {
      "value": 0.5
    },
  },
  "color": {
    "red": {
      "value": "#F00"
    },
  },
  "system": {
    "color": {
      "errorMuted": {
        "value": {
          "ref": "color.red",
          "transforms": [
            {
              "type": "alpha",
              "args": {
                "alpha": {
                  "ref": "opacity.half"
                }
              }
            }
          ]
        }
      }
    }
  }
}

"alpha" here is just a function we registered with some named, typed arguments and a return type so we can validate the connections between nodes. I imagine there are lots of super useful out-of-the-box transformations that everyone would find useful, but the ability to register arbitrary custom transform functions in the parsing/resolution API would also be super useful.

One difference that I think could be important between your proposed schema and the one I present here is that, in the example above, the transforms are children of (rather than sibling to) the token's value entry. In this way it's clear, to me at least, that if I create a reference to the errorMuted token's value, I'm not just referencing its color (color.red or #F00) but its post-transformation value (rgba(255 0 0 / 0.5)).

Thanks very much again! Looking forward to following this project

@meodai
Copy link

meodai commented May 25, 2022

I played around with that idea for small design research project for a client of mine:
https://codepen.io/meodai/pen/WNjKzbp?editors=0010 It is implemented differently but the idea is the same.

I like the proposal @c1rrus made using $operations. But In reality I found it a bit difficult to work like that with some designers. Because most had an awesome understanding of color theory but only a basic understanding of how the different color-models work. Thats why I would like to explore something that works a bit like reference- and value-tokens.
Where a $operation would be a string that points to a function that is defined elsewhere, like @markuslangl proposed.

I think it makes the communication across teams and roles a lot easier, because it would allow us to talk about the abstract idea rather than its implementation. Every designer I know, knows what a complementary color is, or the best contrasting color on a certain background. But how to actually implement that can be quite technical, and its implementation sometimes requires pretty complex functions.

I tend to call this concept a "Rule Token" because I identified two main use cases:

A function that produces a "value" just like the proposed operations, but also
a function that returns a reference for example `cardText (onCard): "whatever color from collection X that has is the most readable on cardBackground"

@TravisSpomer
Copy link

It's occurring to me that we might not have explicitly specified that any unrecognized nodes in the JSON starting with $ MUST be ignored by parsers, though the "Character restrictions" section does specify that token names can't start with $ so maybe that's sufficient. It sounds like probably someday we'll want some top-level spot in the file for defining rules or computations or other things like that ($refs? $rules?), and we should make sure that parsers don't try to treat that as a token group with an invalid name.

@bheston
Copy link

bheston commented Jun 22, 2022

Something along the lines of what's being discussed here is critical to the work I'm doing right now as well. I'm working on FAST (Microsoft's web components foundation) including a design system capability we call Adaptive UI. As mentioned in #81, design systems are really a description of relative choices. Unfortunately, a lot of the ways we've been able to express this in the past have lost track of that fact, for instance, stateful color swatches being expressed as fixed indexes on a manually created color palette without the logic of how we got there.

The system we've built (and are now evaluating how to evolve to this common format) is algorithm centric. Keeping with color, in our case a palette is generated from code and passed in whole to color "recipes", which operate on the palette and other information like contrast requirements or palette offsets to produce a color or set of colors, like for use in rest, hover, and active states. You define the rules of your design, the system figures out the values. The "adaptive" portion of this is that you can change one of the inputs and in real-time see the outputs update, for instance, for custom theme colors, lighter or darker, increased contrast, etc.

These recipe functions have been written to be general-purpose with configurations, but there's no way they could be replicated with a simple base library of included transformations. This will be the case for any complex design system that someone intends to implement and attempts to describe in a format that primarily supports only references / aliases. We extend the recipes model to other areas as well including typography, density (padding & margins), motion, etc.

We were considering a structure using custom composite types to represent this, though that felt more like it was being forced into that structure than purpose-built for it. Linking to issue #54, which removed support for that citing ease of understanding and implementation. In the composite type model we considered defining a list of attributes that included both inputs and outputs, essentially flattening the structure of a function. We would later reference the output attributes in more specific tokens. See #148 for a related point to this.

The operations examples suggested above mostly take only one parameter, and I believe they all are factored to produce a single value. This won't be a sufficient model for describing all extension points like generating a color palette or a set of stateful colors.

In the spirit of principle two, being "focused, yet extensible", my primary concern with this draft is making sure the scenarios described here are not precluded.

I share the concern in the original post that individual tools or products will work around this and will end up with models that can't universally be supported. It's likely the needs are going to be more complicated than can be expressed in simple terms that all tools can be built to support, but it seems preferable if we have a way to define the customizations we need without diverging from the standard format with the expectation that all tools won't be able to support all scenarios. This is going to be true with extensions anyway, though that model doesn't sufficiently describe the structure for the scenarios in this issue. I think it's probable we can describe any custom processing with the re-addition of the typedef structure and some way to describe that external logic may provide the values.


Building on the above suggestions, I support the idea of being able to register a custom function that a processor knows how to evaluate. Perhaps someday there is a standard on how to load those functions or reference them in the tokens doc similar to how a CSS Paint API worklet is registered.

A more general word like function might fit more scenarios better than operation or transform.

In order to define this in a way that can be expressed and validated within the document, the format for the typedef for custom composite types seems like a good start. It would need to be extended to define input or output attributes, or more reusable, two separate types for input and output.

We'd need to be able to individually reference output attributes in subsequent tokens, as I've described separately as #148. In my stateful recipe example, that would allow for binding the rest color, etc.


I can work on some configuration samples if the base requirements make sense. I was going back and forth on how to express the token $type as either a function, a custom typedef representing the function return type, or some combination like function: palette. The issue is the token is not a function, it's the value from a function. I suspect the idea from typedef will be required to represent this, though I was looking for a lighter-touch approach.

An alternative to this is we add language to the document to indicate that any tools must preserve anything they don't understand, allowing for custom definitions of types and functions in a non-standardized way, which may see some common patterns emerge.

@svgeesus
Copy link

svgeesus commented Jul 8, 2022

Rather than re-inventing your own color modification syntax, have you considered looking at the CSS Color 5 relative color syntax and color-mix() ?

I'm seeing some mentions of modifying hue or saturation in the thread above, without any definitions of what those mean, how they are calculated, or how perceptually uniform they are. For CSS Color 5, the necessary hard work of figuring out the color math has already been done.

@markuslangl
Copy link
Author

From the Web development perspective as Design token users, the CSS Color 5 syntax and functions (like color-mix) feel natural, produce expectable results and would be easy to integrate in our workflow.

But with regards to this syntax there are a few questions open:

1. Where will the operations be defined?

Currently, values of the color type are defined in the draft like this:

Represents a 24bit RGB or 24+8bit RGBA color in the sRGB color space. The $type property MUST be set to the string color. The value MUST be a string containing a hex triplet/quartet including the preceding # character.

As it is already discussed in #137, the goal is both supporting colors in other color spaces than sRGB but also limiting the scope of the v1 spec, which might be conflicting. I'd like to avoid moving the discussion from that issue over here, but would like to just mention that maybe the decision of where we want to put the modifications might also influence the decision how we want to define colors in tokens themselves:

1.1. Do we need a new $type for relative colors?

If we'd use the relative color syntax as an actual value of the token, then it would define clearly that this token has a modified value. For example:

{
  // Example base color
  "base": {
    "$type": "color",
    "$value": "#ff0000"
  },

  // Using the relative syntax to get the color to #7f0000
  "brand": {
    "$type": "color",
    // Note: Example syntax, it will be discussed in 2.1. further down
    "$value": "rgb(from {base} calc(r / 2) g b)"
  }
}

But this would break with the current definition of a color value, so maybe it might be preferred to use another type for this, e.g. relative-color or color-relative?

{
  // "base": { "$value": "#ff0000", ... } from above

  // Using the relative syntax to get the color to #7f0000
  "brand": {
    "$type": "relative-color",
    "$value": "rgb(from {base} calc(r / 2) g b)"
  }
}

This might also help Design tools to store relative colors differently than "regular" colors, but it might also cause some additional steps for them to just "switch" between these two steps. How can a Design tool abstract the complexity of defining colors in an easy way for users to just say: "I want to change this color to be 15% lighter than another base color?"

1.2. How do we compose multiple operations?

Of course, if we're adding a new $type for parsing relative colors, we would also need more types for other operations. As we also thinking of supporting the use case of lighten/darken a color (with color-mix()), we might need a special type for color-mix now, and then in the feature maybe even CSS Color 6 color-contrast to define an accessible text color for the given list of defined background colors.

If we want to combine multiple operations with regular CSS syntax, it might become quite hard to parse a single string. For example, if a user wants to lighten a color (which has 50% more green) by 10%, then it could look like this:

{
  // Example base colors
  "white": {
    "$type": "color",
    "$value": "#ffffff",
  },
  "darkgreen": {
    "$type": "color",
    "$value": "#007f00",
  },

  // Target color with example `color-mix` type
  "brand": {
    "$type": "color-mix",
    // Note: Example syntax, it will be discussed in 2.2. further down this comment
    "$value": "color-mix(in rgb, rgb(from {darkgreen} r calc(g + 0.5) b), {white} 10%)"
  }
}

Would it maybe help to reduce the parsing (and reading?) complexity by defining the modification as an $operation instead?

{
  // "white": { ... }, "darkgreen": { ... } from above

  // Target color with example `color-mix` type
  "brand": {
    "$type": "color",
    // Input value
    "$value": "{darkgreen}",
    "$operations": [
      // "$value" refers to original $value, as it is the first calculation
      // At this point it would be #007f00, the original value of {darkgreen} above
      {
        "color-relative": "rgb(from $value r calc(g + 0.5) b)",
      },
      // "$value" refers to current value after the previous calculation
      // At this point it would be #00ff00
      {
        // Note: Example syntax, it will be discussed in 2.2. further down
        "color-mix": "in rgb, $value, {white} 10%"
      }
    ]
  }
}

The $value would be a placeholder which referes the current value at each step of the operation. (It could of course also be written/defined in another way, this is just an example)

By using the $operations array, we can easily define a base color in the $value and do some calculations on it. On the other hand, since the $value is just another color type itself, it might be unintuitive to refer to {brand} in other tokens: Is it just the original $value, or the calculated value (in this case #181818)? And would it be possible to also refer to the original value on demand (e.g. via {brand#value} or {brand.$value})? This also refers to issues #148 and #126.

Another benefit of an $operations array would be, if we could re-use them as separate tokens as described above:

{
  "lighten-soft": {
    "$type": "operation",
    "$value": [
      {
        "color-mix": "in rgb, $value, {white} 10%"
      }
    ]
  },
  "brand": {
    "$type": "color",
    "$value": "{darkgreen}",
    "$operations": [
      // Regular operation
      {
        "color-relative": "rgb(from $value r calc(g + 0.5) b)",
      },
      // Referenced operation
      "{lighten-soft}"
    ]
  },
}

2. Syntax

2.1. Variables

The CSS syntax uses CSS variables for referring to other variables in the color calculations. Would it be helpful to use the Design token syntax for Aliases / references to refer to other Design tokens inside the file?

This would provide the benefit of keeping the same syntax for referring to other values within the same file, and also enabling CSS only users to refer to their actual values (if the translation tools would keep them "as is").

For example:

{
  "base": {
    "$type": "color",
    "$value": "#ff0000"
  },

  "brand": {
    "$type": "relative-color",
    // This would equal the same as the CSS definition...
    // rgb(from var(--base) calc(r / 2) g b)
    // ...which would resolve to...
    // rgb(from #ff0000 calc(r / 2) g b)
    "$value": "rgb(from {base} calc(r / 2) g b)"
  }
}

2.2. Function names

Depending on our decisions on composition and additional types, there would also be different options for defining the respective functions:

In 1.1., if we decide to just add modifications for the color type, then we would need to define the operations/functions in the value itself:

{
  "regular-color-example": {
    "$type": "color",
    "$value": "#007f00",
  },
  "relative-color-example": {
    "$type": "color",
    "$value": "rgb(from {regular-color-example} r calc(g * 2) b / 50%)"
  },
  "color-mix-example": {
    "$type": "color",
    "$value": "color-mix(in rgb, {relative-color-example} 50%, white 10%)"
  }
}

This format would be consistent, especially if we were for example deciding to define regular colors as a CSS Color 4 <absolute-color-function> in #137:

{
  "css4-color-example": {
    "$type": "color",
    "$value": "rgb(0% 50% 0%)",
  }
}

On the other hand, if we decide to use the $operations array, maybe we don't need to define the color function, as it is already the name of the operation?

{
  "color-mix-operation-example": {
    "$type": "color",
    "$value": "{darkgreen}",
    "$operations": [
      // Should we just define the function arguments in the value...
      {
        "color-mix": "in rgb, $value, {white} 10%"
      },
      // ...or also repeat the function inside the value...
      {
        "color-mix": "color-mix(in rgb, $value, {white} 10%)"
      },
      // ...or use a generic operation name for color modifications?
      {
        "modify": "color-mix(in rgb, $value, {white} 10%)"
      },

      // The latter could then also be used for relative colors:
      {
        "modify": "rgb(from $value r g b / calc(alpha - 10%))"
      },
    ]
  }
}

3. Portability

Using this in the web would be fairly easy: A translation tool could just replace the Aliases/references with the actual calculated values and export a combinated token. A transformed result for the first example in 2.1. could be:

:root {
  --regular-color-example: #007f00;
  --relative-color-example: rgb(from var(--regular-color-example) r calc(g * 2) b / 50%);
  --color-mix-example: color-mix(in rgb, var(--relative-color-example) 50%, white 10%);
}

The question would be how easy this could be ported to other platforms (as e.g. games, mobile, print). Would it be expected for a translation tool to be able to calculate the end result of a relative color or color-mix function?

Considerations

From our perspective, having just a single color type which supports all of the regular CSS values would be the most consistent way to define colors. There's no need to wonder "Which type do I need?", "I need to mix different tokens together, how do I do it?", "What's the generated markup from the translation tools?".
On the other hand, declaring some generic operations as tokens themselves and being able to re-use them, would be quite a common use-case as well, so maybe a combination of both could provide a very flexible solution?

But of course all of this might increase the burden for implementers (such as Design tools and Translation tools). If we can reduce the complexity for them, it might also increase the adoption (as the specification might look less intimidating for new developers)...

@ddamato
Copy link

ddamato commented May 31, 2023

Note

Not sure if it's appropriate to raise here but since I first found the concept here I'll leave a comment. Folks can let me know if this is significant enough for it to be a separate issue.

There's been a few asks for the ability to refer to a token and transform it before finally resolving the value to downstream platforms. As an example, taking a primary color and adjusting the saturation for the hover color. I happened upon this very issue and saw the idea of an $operations key and I was fixated on it all weekend. After some thinking I've took a stab at it with some of my own requirements:

Operations should have a limited, low-level set of agreeable implementations

An alpha implementation can differ between vendors and I don't think we're prepared to define all of the possible operations here in this specification. It would mean vendors would take time to implement each as needed, and compatibility could become out of sync as some vendors are faster to implement than others.

It would be more manageable to agree on low-level primitives, most of which already exist on String and Math which can be composed into high-level reusable operations.

Operations should be highly customizable

Color isn't the only kind of token that people are looking to transform. Creating a typography scale could be done using this method by supplying a base and scale. Other tokens could be computed if given the ability to do so.

Collections of operations should be reusable

With a low-level sort of language, we'll need to provide abstractions that can be reused with a reference to those operations. Importing these operations can spread the steps in place.


The following example is very verbose but the blog post above has a recommendation for a reusable abstraction.

{
  "primary-color-overlay": {
    "$type": "color",
    "$value": "#fffc00",
    "$operations": [
      0.5,
      ["String.match", "$value", "#([0-9A-Fa-f]{2})"],
      ["String.match", "$value", "#(?:[0-9A-Fa-f]{2})([0-9A-Fa-f]{2})"],
      ["String.match", "$value", "#(?:[0-9A-Fa-f]{4})([0-9A-Fa-f]{2})"],
      ["Math.parseInt", "$1", 16],
      ["Math.parseInt", "$2", 16],
      ["Math.parseInt", "$3", 16],
      ["String.concat", ",", "$4", "$5", "$6", "$0"],
      ["String.concat", "", "rgba(", "$7", ")"]
    ]
  }
}

See minimal prototype in action.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dtcg-color All issues related to the color module specification. dtcg-format All issues related to the format specification. Needs Feedback/Review Open for feedback from the community, and may require a review from editors.
Projects
None yet
Development

No branches or pull requests