Skip to content

[Feature]: Research and concept color tokens #594

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

Closed
3 tasks
Tracked by #583
andrzejewsky opened this issue Sep 21, 2023 · 15 comments · Fixed by #656
Closed
3 tasks
Tracked by #583

[Feature]: Research and concept color tokens #594

andrzejewsky opened this issue Sep 21, 2023 · 15 comments · Fixed by #656
Assignees
Labels
backlog feature next Next version of Macaw UI rfc
Milestone

Comments

@andrzejewsky
Copy link
Member

andrzejewsky commented Sep 21, 2023

In macaw UI we have theme system, that provides design tokens. Each token represents some value: eg color value, font size value, font weight value end etc. Focussing on the colors, we are also distinguishing them depending its context. Given twe have colors for borders, bachgrounds, foregrounds and so on.

Since it works fine in general, but creates a problem, problem of choice. The end user, doesn't know which color use to be contrasted with the other. As example: which color of font or background to use to be contrasted on another background.

We need to reveal what we want to approach this problem. As example we can consider color hierarchy where each part of token name represent the layer in tree:

Schema: --[type]-[element]-[role]-[variant]-[state]

--color-bg

--color-bg-error
--color-bg-interactive


--color-bg-interactive-neutral
--color-bg-interactive-subbdued

--color-bg-interactive-neutral-hover
--color-bg-interactive-neutral-focus
--color-bg-interactive-subbdued-hover
--color-bg-interactive-subbdued-focus

In this context, going from down to up colors are always contrasted, when used accordingly to hierarchy.

AC:

  • Define how we want to approach design tokens for colors
  • Prepare some concepts (code or/and figma) that proves usability
  • Show how contrasted colors are and how we obtain their values (by hand? we generate it?)
@andrzejewsky andrzejewsky added the next Next version of Macaw UI label Sep 21, 2023
@github-project-automation github-project-automation bot moved this to 🆕 New in MacawUI Sep 21, 2023
@krzysztofzuraw krzysztofzuraw moved this from 🆕 New to 🏗 In progress in MacawUI Sep 26, 2023
@andrzejewsky andrzejewsky added this to the 1.0.0 milestone Sep 26, 2023
@krzysztofzuraw
Copy link
Member

I did a small research and play with different approaches to design systems

Shopify Polaris

Their system uses --[type]-[element]-[role]-[variant]-[state] pattern of naming tokens:

--color-bg
--color-bg-interactive
--color-bg-interactive-subdued
--color-bg-interactive-subdued-hover

--color-text
--color-text-brand-on-bg-fill-hover

--color-border
--color-border-brand
--color-border-emphasis-active

--color-icon
--color-icon-secondary-active

The following usage is applicable in the case of a dashboard:
shopify

Cal.com

Their system uses --[type]-[element]-[variant] pattern of naming tokens:

--color-bg-default
--color-bg-subtle
--color-bg-info

--color-border-default
--color-border-subtle

--color-content-default
--color-content-subtle
--color-content-info

--color-brand-default
--color-brand-subtle

The following usage is applicable in the case of a dashboard:
cal

@andrzejewsky
Copy link
Member Author

andrzejewsky commented Oct 13, 2023

Ok, doing this research and experimenting with the structure, we are able to define the following spec of color tokens:

Type Variant State Value
background default x
background default hover
background default focus
background muted x
background muted hover
background muted focus
background strong x
background strong hover
background strong focus
background inverted x
background disabled x
background success x
background info x
background critical x
background warning x
text default x
text muted x
text strong x
text inverted x
text disabled x
text success x
text info x
text critical x
text warning x
border default x
border default hover
border default focus
border muted x
border muted hover
border muted focus
border strong x
border strong hover
border strong focus
border inverted x
border disabled x
border success x
border info x
border critical x
border warning x
icon default x
icon muted x
icon strong x
icon inverted x
icon disabled x
icon success x
icon info x
icon critical x
icon warning x
shadow default x
shadow default focus
shadow default hover

1. Schema

Each color token follows schema of [type]-[variant]-[state] where:

  • type - is the color component used within the css/html, eg. color for background, border, icon, box shadow
  • variant - is the identifier that position the color within hierarchy (point 2)
  • state - defines the states such as hover or focus

Currently defined types:

  • background - used for any background color within the ui
  • border - used for all of borders
  • icon - used in icon backgrounds
  • shadow - dedicated for box shadows

Currently defined variants:

  • general purpose:
    • default - represents default color for the biggest areas within the app, such as app background, center content color. The app starts with this color
    • muted - elements that are placed on top of the app, if we want to emphasise/differenciate them eg. sidebar, top nav
    • strong - elements placed either on default or muted and we want user to pay attention to them. Used in section headers, important information, call to actions etc.
    • inverted - elements that are placed on top of muted elements
  • context related (they are self explanatory, also they are the most vibrant colors):
    • disabled
    • info
    • success
    • critial

2. Variant hierarchy

The hierarchy follows the orders:

Used variant Can use on it
default muted, strong, disabled, info, success, critical
muted strong, disabled, info, success, critical
strong inverted, disabled, info, success, critical
inverted disabled, info, success, critical
disabled x
info x
success x
disabled x
critical x

3. Recap / Questions

  • Here is the pull request that also presents the idea, including demo in storbook
  • Names of variants can be re-defined, things like "muted" / "strong" may not sound descriptive
  • Shadows are defined just for default, we realise that it's enough, but we leave space for further changes here
  • Icons have dedicated colors, good practice is to have a little bit different shade of the icon color in relation to text it comes with
  • We don't need state (hover/focus) colors for everything, we can toogle border/background, that seems enough

@krzysztofzuraw
Copy link
Member

After yesterday's discussion, we concluded that more complex components like Selects should allow the changing of color properties (backgroundColor, color, and borderColor), but only when necessary. For example, in the snippet below, developers should only be able to change the color for the label element, not the options or select:

const Select = ({ color }) => {
  return (
    <select>
      <label color={color}>Select item</label>
      <option>Flowers</option>
      <option>Furniture</option>
    </select>
  );
};

<Select color="muted" />;

To achieve this, we need to establish boundaries for the more complex components (Input, Combobox, Select, and Multiselect) in MacawUI, allowing developers to change colors only in specified parts of those components.

@krzysztofwolski
Copy link
Member

krzysztofwolski commented Oct 18, 2023

I'll give some examples from the apps where I had to use colors.

Custom card with "dangerous" (non-reversible actions)

image

Different button states

"Base" styles of the buttons:

  • primary
image
  • critical
image
  • tertiary
image

I would like to define states

  • active (and on hover if possible)
  • disabled
  • in flight (probably can be the same colors as disabled, just with different message)

Custom chip components

image

Let's say I want to create more color options. I would appreciate some guidance on how to choose colors.

@krzysztofwolski
Copy link
Member

Meeting notes:

  • I'm looking forward to a new color scheme with the proposed tokens 🎉
  • DX vise big improvement too - names are clear and better understandable
  • Thanks for the storybook examples since those are the best to present usability and real-life usage :)

Possible issues:

  • Buttons in three "levels" (primary/secondary/tetiary), each with different color (critical/success/default) and state (default/disabled/hover)
  • how to approach "dangerous section" (screenshot in the previous post) - since this pattern occurs in a few apps and projects, we should prepare a recommended look
  • some of the components (chip) might want to use broader color palette , we might introduce it later

@witoszekdev
Copy link
Member

Really like the changes, specially making more strict which colors are accepted in what Component 🚀

Feedback from me: I would rethink our color naming. muted color isn't actually less visible, than default, I think it should be named brand or something like that.
It would be great to have a storybook that describes in which situations we should use which colors.

In forms, we usually have a label and a description field beneath the input. Let's say the form has bg critical and text is also critical. We'd like our describedby text to have a bit of different color than the rest (i.e. more muted but not in the current understanding of macaw). I think we should another color variant for cases like that.

@lkostrowski
Copy link
Member

3 cents on components colors. I think we should have following values layers:

  1. Raw value with optional color alias. e.g. #ff0000 or --red: #ff0000
  2. Generic application level values, eg --color-bg-interactive-neutral-focus: #ff0000 or --color-bg-interactive-neutral-focus: var(--red)
  3. Component colors mapping, eg --color-component-button-border-hover: var(--color-bg-interactive-neutral-focus)

I think we should style components with variables, not props. By default Macaw will style them with proper usage and for majority of users knowing tokens will be not necessary, because there will be semantic aliases for everything.

So my 3 cents is to ensure components will have aliases. So when we change button's border, we only change its local mapping, not entire theme

@andrzejewsky
Copy link
Member Author

andrzejewsky commented Nov 9, 2023

Ok, to recap all of feedback and research.

Definition of tokens

Seems like we can simplify the tokens and instead doing sophisticated naming and rules, we can just do the mapping. I would see three levels of tokens: raw token, semantic level and component one:

We have the following tokens:

/* raw colors */
--red: red
--blue: blue
--green: green

/* semantic colors */

--background-default: var(--red)
--background-muted: var(--green)

/* component level mapping */
--bg-button: var(--background-default)

Now button implementation and usage:

const Button = (props) => <Box backgroundColor="bg-button" {...props} />

<Button>Click me!</Button> // by default it's red

<Button backgroundColor="background-muted">Click me!</Button> // I change color, but still based on semantic colors

<Button __backgroundColor="#3cb371">Click me!</Button> // in the very specific place i want it to be shade of green

By doing this, the developer is able to:

  • change the global styling for one component, eg. all buttons (use case: i want to update brand color for my business, i want to do it just for buttons, don't need to update colors of anything else)
  • When default colors for a given component doesn't work, i can always reference the semantic ones and still follow guidelines
  • Override always: if some of tokens doesn't really work, you can override it locally, (use case: creating a button that is specific to this particular place)

Raw tokens naming and structure

Raw colors can take structure of: --[name]-[shade]

where:

  • name - name of the color, indicitate its value
  • shade - numer that represents it shade

Examples: --gray-100, --gray-200, --red-100

Semantic tokens naming and structure

Layer that represents abstraction for colors for variety of elements on the UI eg. borders or backgrounds.

--color-[yype]-[variant]-[state]

where:

  • name - is name of css elelemt: border, background, color etc.
  • variant - name of color variant eg: default, inteeractive, muted etc.
  • state - indicates state of given element: eg hover, focus

Example:

--s-color-background-default: var(--gray-100);
--s-color-background-muted: var(--gray-50);
--s-color-background-strong: var(--gray-400);
--s-color-border-default: var(--red-200);

Component tokens naming and structure

Considering states, variants and use cases, based on our discussions, the following schema naming would be suitable:

--s-color-[component]-[type]-[state]
where:

  • component - is the name of macaw component eg. button, text, dropdown
  • variant - if some component has variant, variant name is on second place (eg. button-primary)
  • type - is name of css elelemt: border, background, color etc.
  • state - indicates state of given element: eg hover, focus

Example:

--c-button-primary-background: var(--color-background-default);
--c-button-primary-border: var(--color-border-default);
--c-button-primary-background-hover: var(--color-background-muted);
--c-button-primary-background-focus: var(--color-background-strong);

@timuric timuric added the rfc label Nov 10, 2023
@timuric
Copy link
Contributor

timuric commented Nov 10, 2023

I have the following concerns regarding the 3rd layer of tokens —c-[component]-[variant]-[state]:

  • The amount of tokens lets take 30 components, with an average variant range of 5, and 5 states, this results in 750 tokens, which is not something easy to document
  • Tokens are singletons, meaning that throughout the code base it will be used once. This means that there is no difference between changing value of a border in a component vs mapping. In fact it introduces unnecessary steps
  • It is not extensible, users of the design system can’t add or reuse such tokens in most cases
  • Some app makers (public consumers) might create nonsense mapping, for example a calendar component that uses tokens of a card in their own calendar component, because they are somehow similar
  • Learning system would be tedious, you can’t just inspect an element and learn the value without jumping through out the hoops
image

Persona: Design system user

  1. The developer wants to create a new button with animation because the design team believes that Call to Action of their app should spark joy. This means that the button needs to be created from scratch. Developer writes something like this:
const AnimatedButton = () => {
    const styles = {
        border: "var(--button-primary-default)"
    }
    return <div styles={styles}></div>
}

The developer realizes that the semantics don't match because his button is neither primary nor secondary, its a new component that the design system doesn't have. Even if he would treat this component as primary button (ignoring the component name mismatch), the state animated doesn't exist either.

The developer decides to rewrite this using raw token structure --[name]-[shade]
He just needs to find out the raw color values, he inspects existing component to see the raw value:

image

However, the underlying variables are not useful and require to go to the definition ಠ_ಠ

Persona: Maintainer

Design team came up with new component, developer declare a token that maps to the value in figma and type the variable name in component, resulting in multiple file edits instead of directly mapping the value in component

@lkostrowski
Copy link
Member

lkostrowski commented Nov 10, 2023

Answering some:

The amount of tokens lets take 30 components, with an average variant range of 5, and 5 states, this results in 750 >tokens, which is not something easy to document

Such things can be autogenerated from theme. Actually each value is a "private field" for a component, so the only reason to expose it is to adjust theme. E.g. "change background of Modal, without changing all backgrounds". So only need to document is to expose available tokens

Tokens are singletons, meaning that throughout the code base it will be used once. This means that there is no >difference between changing value of a border in a component vs mapping. In fact it introduces unnecessary steps

The middle step is to avoid globals. e.g.

--color-background-default: #fff

Then, there is a Modal

<Box backgroundColor="colorBackgroundDefault" .../>

Then, I want to change modals to be slightly darker

--color-background-default: #eee // oops, all backgrounds just changed

Instead:

<Box backgroundColor="compModalBackground" .../>
// Macaw theme
--compModalBackground: var(--color-background-default);

// User code
--compModalBackground: var(--color-background-darker);

It is not extensible, users of the design system can’t add or reuse such tokens in most cases

Why would they?

  1. If they want new component in Macaw, they contribute to Macaw
  2. If they write custom components, they use semantic tokens like --color-background-default or raw color values

Some app makers (public consumers) might create nonsense mapping, for example a calendar component that uses tokens >of a card in their own calendar component, because they are somehow similar

Tokens for components will be private for components. Tokens for "card" will not compile under "calendar" (at least how we discussed it with @andrzejewsky )

Learning system would be tedious, you can’t just inspect an element and learn the value without jumping through out the hoops

I don't get it, this is part of using CSS variables?

The developer realizes that the semantics don't match because his button is neither primary nor secondary, its a new component that the design system doesn't have.

The developer should introduce new color because he add new component

@timuric
Copy link
Contributor

timuric commented Nov 10, 2023

Such things can be autogenerated from theme. Actually each value is a "private field" for a component, so the only reason to expose it is to adjust theme. E.g. "change background of Modal, without changing all backgrounds". So only need to document is to expose available tokens

It doesn't change the fact that you endup mapping hundreds of tokens

Then, I want to change modals to be slightly darker

To change the modals you change modals mapping

<Box backgroundColor="gray-100"/>

To change the theme you change --color-background-default

I don't see how these examples justify the need for singleton tokens, or which problem it solves. Your examples are only relevant if the token is used twice which is not the case.

If they want new component in Macaw, they contribute to Macaw

That is not purpose of a design system, and neither we can accept every component that app makers need

If they write custom components, they use semantic tokens like --color-background-default or raw color values

Exactly, that is why having native components using something else makes it harder to reference and learn the system

The developer should introduce new color because he add new component

Developers should use colors from the theme, you can't introduce new color without breaking out from the theme

@timuric
Copy link
Contributor

timuric commented Nov 10, 2023

After some discussion with @lkostrowski it turns out one of the motivations to have singletons is to override the theme. However if any theme can change the rules of mappings we loose any predictability and will create a risk of apps looking different.

Example:
User creates a theme where primary buttons have outline style, but since apps are using raw tokens directly custom components would not be aware of such customization and look different from the theme

@krzysztofzuraw
Copy link
Member

krzysztofzuraw commented Nov 17, 2023

After conducting further research, we have decided to adopt the following approach for naming tokens. We will have two types of tokens for colors: design system tokens and tokens for semantic components.

Design system tokens

Design system tokens will have following structure:

--mu-[type]-[element]-[role]-[layer]-[state?]

Where:

  • type is color
  • element is one of: background | text | border
  • role is one of: default | critical | info | success | warning | accent
  • layer is one of: 1 -> 2 -> 3 | disabled
  • state is one of: hover | focused

Layers

Layer number 1 is used as default application color e.g dashboard background. Layer number 2 is used for e.g dashboard sidebar color. User can use layers on top of the each other in one direction: 1 -> 2 -> 3. Disabled is special layer for disabled elements

State

Different variations for hover or focused states

Semantic components tokens

Semantic components tokens will have following structure:

--mu-[type]-[element]-[role]-[variant]-[state?]

Where:

  • type is color
  • element is one of: button | button-text
  • role is one of: default | critical | info | success | warning | accent
  • variant is one of: primary | secondary | tertiary
  • state is one of: hover | focused

Note

Inputs for now will have transparent background. Text and border will be using layer 1.
In the future we cloud add tokens just for input.

State

Different variations for hover or focused states

Tokens table

CSS Token Box prop & token
—-color-background-default-1 backgroundColor="default1"
—-color-background-default-1-hover backgroundColor="default1Hover"
--color-background-default-1-focus backgroundColor="default1Focus"
--color-background-default-2 backgroundColor="default2"
--color-background-default-2-hover backgroundColor="default2Hover"
--color-background-default-2-focus backgroundColor="default2Focus"
--color-background-default-3 backgroundColor="default3"
--color-background-default-3-hover backgroundColor="defalut3Hover"
--color-background-default-3-focus backgroundColor="default3Focus"
--color-background-default-disabled backgroundColor="defaultDisabled"
--color-background-critical-1 backgroundColor="cricital1"
--color-background-critical-2 backgroundColor="critical2"
--color-background-info-1 backgroundColor="info1"
--color-background-info-2 backgroundColor="info2"
--color-background-success-1 backgroundColor="sucess1"
--color-background-success-2 backgroundColor="success2"
--color-background-warning-1 backgroundColor="warning1"
--color-background-warning-2 backgroundColor="warning2"
--color-background-accent-1 backgroundColor="accent1"
--color-background-accent-2 backgroundColor="accent2"
   
--color-text-default-1 color="default1"
--color-text-default-2 color="default2"
--color-text-default-3 color="default3"
--color-text-disabled color="disabled"`
--color-text-critial-1 color="critical1"
--color-text-info-1 color="info1"
--color-text-success-1 color="success1"
--color-text-warning-1 color="warning1"
--color-text-accent-1 color="accent1"
   
--color-border-default-1 borderColor="default1"
--color-border-default-1-hover borderColor="default1Hover"
--color-border-default-1-focus borderColor="default1Focus"
--color-border-default-2 borderColor="default2"
--color-border-default-2-hover borderColor="default2Hover"
--color-border-default-2-focus borderColor="default2Focus"
--color-border-default-3 borderColor="default3"
--color-border-default-3-hover borderColor="default3Hover"
--color-border-default-3-focus borderColor="default3Focus"
--color-border-default-disabled borderColor="disabled"
--color-border-critical-1 borderColor="cricital1"
--color-border-info-1 borderColor="info1"
--color-border-success-1 borderColor="sucess1"
--color-border-warning-1 borderColor="warning1"
--color-border-accent-1 borderColor="accent1"
   
--color-button-default-primary backgroundColor="buttonDefaultPrimary"
--mu-color-button-default-secondary backgroundColor="buttonDefaultSecondary"
--mu-color-button-default-tertiary backgroundColor="buttonDefaultTertiary"
--mu-color-button-text-default-primary color="buttonTextDefaultPrimary"
--mu-color-button-text-default-secondary color="buttonTextDefaultSecondary"
--mu-color-button-text-default-tertiary color="buttonTextDefaultTertiary"
--mu-color-button-critical-primary backgroundColor="buttonCriticalPrimary"
--mu-color-button-critical-secondary backgroundColor="buttonCritialSecondary"
--mu-color-button-critical-tertiary backgroundColor="buttonCriticalTertiary"
--mu-color-button-text-critical-primary color="buttonTextCriticalPrimary"
--mu-color-button-text-critical-secondary color="buttonTextCriticalSecondary"
--mu-color-button-text-critical-tertiary color="buttonTextCriticalTertiary"

@lkostrowski
Copy link
Member

Looking good in general, I like 1->2->3 concept

I don't like this:


--mu-color-button-default-tertiary 
--mu-color-button-text-default-primary 

We use the same symbol - as a delimiter AND a join character for element type. This adds some mental burden.

Also, we mix specificity, the first one is generic ("button"), the second is more specific (text is part of button).
But button is actually referring to button background, and this is nowhere in the token name

So maybe we should actually have

--mu-color-button-bg-default-tertiary 
--mu-color-button-text-default-primary 

Optionally we can also add another symbol for component name join, eg

--mu-color-buttonText-default-primary 

or

--mu-color-button_text-default-primary 

@krzysztofzuraw
Copy link
Member

Thanks for the comment - I'm wondering if we should have sematic tokens in this form:

--mu-[type]-[element]-[component]-[role]-[variant]

--mu-color-background-button-default-primary
--mu-color-background-button-default-secondary
--mu-color-background-button-default-tertiary
--mu-color-background-button-critical-primary

--mu-color-text-button-default-primary
--mu-color-text-button-default-secondary
--mu-color-text-button-default-tertiary
--mu-color-text-button-critical-primary

as it will be working nicely with how we group our color tokens inside contract (we sort them into three groups: 'background', 'text' and 'border')

This was referenced Nov 30, 2023
@github-project-automation github-project-automation bot moved this from 🏗 In progress to ✅ Done in MacawUI Dec 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backlog feature next Next version of Macaw UI rfc
Projects
No open projects
Status: ✅ Done
Development

Successfully merging a pull request may close this issue.

7 participants