design-tokens / community-group

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

[Discussion]: How to transform composite tokens #226

Open danlevy1 opened 1 year ago

danlevy1 commented 1 year ago

Description

The latest editor's draft (as of August 2023) states that composite design token is "a design token whose value is made up of multiple, named child values."

Given the following example in the editor's draft, is there currently a recommendation on how to transform the token?

{
  "shadow-token": {
    "$type": "shadow",
    "$value": {
      "color": "#00000088",
      "offsetX": "0.5rem",
      "offsetY": "0.5rem",
      "blur": "1.5rem",
      "spread": "0rem"
    }
  }
}

It seems like there are two options here:

  1. Each value property name is appended to the top-level token name to create a new token, and all of those tokens must be applied
  2. The top-level token name is still a singular value, and the value refers to all sub-values that must be applied

If we take a look at those two possibilities in action, here's what they might look like if we transformed them into JavaScript variables:

// Option 1

const SHADOW_TOKEN_COLOR = "#00000088";
const SHADOW_TOKEN_OFFSET_X = "0.5rem";
const SHADOW_TOKEN_OFFSET_Y = "0.5rem";
const SHADOW_TOKEN_BLUR = "1.5rem";
const SHADOW_TOKEN_SPREAD = "0rem";
// Option 2

const SHADOW_TOKEN = {
  color: "#00000088",
  offsetX: "0.5rem",
  offsetY: "0.5rem",
  blur: "1.5rem",
  spread: "0rem"
}

Tradeoffs

Option 1 Pros:

  1. Tokens can be accessed individually (e.g. apply SHADOW_TOKEN_COLOR, but don't apply SHADOW_TOKEN_OFFSET_X).
  2. Don't need to worry about multiple formats of tokens (i.e. all values will be strings, and there are no object values).

Cons:

  1. It isn't immediately clear which transformed tokens need to be applied together other than the first part of the token name (e.g. SHADOW_TOKEN). This would require token users to be extra careful in order to ensure that all generated tokens coming from the one composite token are applied.

Option 2 Pros:

  1. Only one transformed token is generated.
  2. It's very clear which values need to be applied together.

Cons:

  1. Token users would need to be aware that tokens may come in multiple formats (e.g. strings or objects).
ddamato commented 1 year ago

I've thought about it in relation to the work I'm doing with Token Operations. From my perspective we could have a setup like the following:

{
  "shadow1": {
    "_color": {
      "$value": "#00000088"
    },
    "_offset-x": {
     "$value": "0.5rem"
    },
    ...
    "css": {
      "$operations": [...] // concat all parts together into single CSS value
    },
    "android": {
      "$operations": [...] // concat all parts together into single android appropriate value
    }
  }
}

So a final resolution for CSS would be available at {shadow1.css} which could be written within the UI directly without further transformation. This would avoid the responsibility of the spec to outline all composite type schemas and put the responsibility on the folks who need specific values to have them available as needed. As an example, perhaps a tokens file is only used for android values. The system ingesting them won't need to know how to combine composite types; everything is already exactly made as needed when it is accepted as a value; right within the file.

ipaintcode commented 1 year ago

I've been digging into these different options for managing composite design tokens, and here are my two cents.

Option 1, which decomposes each token into individual variables, offers granular control. This allows for fine-tuned customization but could lead to management complexities, especially when it comes to ensuring the proper combination of these decomposed tokens.

Option 2 encapsulates all attributes into a single object and provides a cohesive approach. It makes it clear which attributes are meant to be used together but may not offer the same level of granularity that might be required in some scenarios.

However, the 3rd approach suggested by @ddamato provides a balanced solution. It still allows for granular customization via sub-properties while providing a consolidated output tailored for specific platforms like CSS or Android through token operations. This modular approach not only streamlines the application of design tokens but also enables more efficient cross-platform compatibility.

Given the flexibility and scalability inherent in the third approach, it is a compelling solution for managing composite design tokens effectively.

c1rrus commented 1 year ago

Does the spec need to have an explicit view on this at all?

In my mind composite tokens are a bit like "styles" in Figma (or equivalents in other design tools). They're a structured set of values that, in design tools, are usually selected and applied as a single unit. I think they still make sense as "design tokens" though, as they're still named design decisions and likely to be part of a design system team's vocabulary.

When it comes to translating them to code, there are several ways they might be represented. On some technology platforms, there might be an analogous "group" construct that lets you apply them all at once, in others there may not. And sometimes both ways might be possible.

The 2 options in the OP demonstrate this exactly. I'd argue which one you choose (why not both?) is up to your team.

The same can be true in other programming languages. For example, the shadow token from the OP in CSS could be expressed as a single CSS var:

:root {
  --shadow-token: 0.5rem 0.5rem 1.5rem 0rem #00000088;
}

/* application */
.foobar {
  box-shadow: var(--shadow-token);
}

But it could just as well be "exploded" into its individual sub-values:

:root {
  --shadow-token-offset-x: 0.5rem;
  --shadow-token-offset-y: 0.5rem;
  --shadow-token-blur: 1.5rem;
  --shadow-token-spread: 0rem;
  --shadow-token-color: #00000088;
}

/* application */
.foobar {
  box-shadow:
    var(--shadow-token-offset-x)
    var(--shadow-token-offset-y)
    var(--shadow-token-blur)
    var(--shadow-token-spread)
    var(--shadow-token-color);
}

Neither is right or wrong. It just comes down to individual teams' preferences and requirements.

While I'll admit that exploding the parts of the shadow token is something folks are unlikely to do as shadow values in CSS always seem to be applied as a unit, the same isn't necessarily true for something like a border which might be applied all at once as a shorthand value or individually using the long hand properties:

.shorthand {
  border: var(--border-token);
}

.longhand {
  border-color: var(--border-token-color);
  border-width: var(--border-token-width);
  border-style: var(--border-token-style);
}

Or indeed a typography token which, at least in CSS, would have to be exploded into individual values because there's no shorthand property that covers them all:

:root {
  --typography-token-font-size: 1rem;
  --typography-token-font-family: Comic Sans MS;
  --typography-token-font-weight: bold;
  --typography-token-line-height: 1.4;
  --typography-token-line-height: 1.4;
}

.heading {
  font-size: var(--typography-token-font-size);
  font-family: var(--typography-token-font-family);
  /* and so on */
}

For that particular type, you could event export the whole token as a CSS class (or a Sass mixin):

.typography-token {
  font-size: 1rem;
  font-family: Comic Sans MS;
  /* and so on */
}

I suppose a DS team might even export grouped and exploded versions to give downstream product teams the freedom to choose which one(s) they want to use.

My point is, there are potentially many, valid ways to express the DTCG composite types in different programming languages. I don't believe the spec should mandate a particular way. I'd therefore argue it's fine to let that be a choice or a configuration option for tools.

ddamato commented 1 year ago

nit:

Or indeed a typography token which, at least in CSS, would have to be exploded into individual values because there's no shorthand property that covers them all:

There is, the font property is a shorthand for all of these:

My concern for the spec would be that any point some new property that requires composite tokens to appear, would require the definition of that composite to exist and then distributed across tools in varying amounts of support. It is fairly clear the reason why shadow has been given a special composite status is because it is not a shorthand in CSS, making many of the values required and therefore standardized. In the font example, while none of those values are required, I would say it is common for them to be used together. As an example defining a typographic style commonly expects a family and size together at least. In fact, the font shorthand requires family and size at minimum, and additionally has a special ordering of values afterward.

I'd also be concerned about misunderstanding of use, where the shadow token is applied to the CSS filter: drop-shadow() function which does not accept the spread value like the box-shadow version. In the Token Operations would we (the author) define what the final value should be for the platform to use instead of the platform waiting for a specification decision and backlogged implementation. So we may provide the CSS filter version of the value and box-shadow version of the value as needed. In this way, the destinations are only reading primitive values with no black boxes or backlogs.

ilikescience commented 11 months ago

I have a very strong preference for keeping all of the values of a composite token together. This is faithful to the nature of a design token as being a design decision; there are some places where you have to make multiple decisions to get to the lowest level of fidelity, eg shadows or type. You can't just decide what a shadow's color is, you also have to decide what it's blur radius, x, and y offset are. Likewise you can't just decide what at piece of text's font family is, you also have to decide it's size, etc.

However, like @c1rrus said, I think that the decision as to how to transform the token is up to the transformer, and the spec shouldn't take a stance.

As for @ddamato's follow-up, I put some thoughts in #100 on the issue of platforms having multiple incompatible syntaxes for the same type of token (tl;dr, I think that's another translator concern that the spec shouldn't weigh in on).