Skip to content

tokens component

Ali Stump edited this page Oct 22, 2024 · 17 revisions

tags: [tokens]

Component Tokens

The Goal

Component tokens define design pattern decisions at the component level. They meet user requirements for a clear and useful component theming and overrides.

While it is possible to create a variable for each style property we need to balance customization with scale and maintainability. Allowing customer white-labeling that makes Calcite Components feel like a part of their apps while maintaining the underlying Calcite design patterns. To this end, all component tokens should reference/fallback to semantic and global tokens.

Naming Pattern

Read more on the full naming pattern schema.

Most component tokens use the following schema parts... System (calcite), tier (web-platform), component, element, type, group, and state.

Naming rules

General naming rules

  • Create as few new component tokens as possible to define the component design pattern and meet approved user use-cases.
  • Tokens should be based on the design spec not the component API.
  • The system will always be "calcite"
  • The tier will always be "web-platform". Component tokens are currently only available for the web platform as CSS variables through Calcite Components. As with other Calcite tokens when converted to CSS variables the tier will not be included.
  • [component] and [type] parts are always required.
  • [state] is only required when targeting styles of a sub-component or element within the component.
  • When a pattern is shared between related components a single token may be used.

Stateful naming rules

  • Token state should be "hover", "press" or nothing.
  • Having tokens with state is only necessary when targeting sub-components with a state which changes unique from the parent component.
  • when targeting pseudo classes use the present tense. -hover -focus -press.
  • A state that is shared by :hover and :focus should use -hover
  • A state shared by :focus and :active should use -press
  • A state which includes :active and/or when it's possible to group in component properties like [selected] and [checked], use -press
  • In edge cases where a design pattern may require an additional token for targeting properties like [checked] or [selected] that can not be covered by a -press token, use the past tense. -checked -selected

Applying tokens to a component

Component tokens decision tree

flowchart TD
    add{add a new token. Confirm the naming schema with the Calcite team.}
    use{use existing token}
    override{add a new token and override the subcomponent token}
    deprecate{deprecate the variable}
    pass{do nothing. Sub-component token will pass through the CSS. Note this in the docs.}
    internal{make an internal domain variable}
    nothing{do nothing. Not tokenizable.}

    A1[Looking at a Calcite component's CSS file]
    B1[this component uses <a href="https://github.com/Esri/calcite-design-system/wiki/tokens-component#3-review-component-for-tokenizable-styles">tokenizable styles<sup>*</sup></a>]
    B2[this component is part of a component-group <em>Example: Tabs, Tab-Title, Tab-Nav, Tab</em>]
    C1[this value is used many times and it would be very helpful to have a single variable representing this value.]
    D1[elements in the design pattern share this value]
    D2[these components share a design pattern]
    E1[these elements are sub-components]
    E2[these elements already have a token]
    F1[sub-components are slotted in and/or their use as a sub-component is very clear in the documentation]
    G1[token naming schema is out of date]

    A1 --> B1
    B1 -- No --> C1
    B1 -- Yes --> B2
    B2 -- No --> D1
    B2 -- Yes --> D2
    D1 -- Yes --> E1
    D2 -- Yes --> E2
    D2 -- No --> D1
    E1 -- Yes --> F1
    E1 -- No --> E2
    E2 -- Yes --> G1
    C1 -- Yes --> internal
    C1 -- No --> nothing
    F1 -- Yes --> pass
    F1 -- No --> override
    G1 -- Yes --> deprecate
    G1 -- No --> use
    D1 -- No --> add
    E2 -- No --> add
    deprecate --> add
    
Loading

In this example we are following the CSS variable fallback pattern. This will default to a transparent background-color but will allow consuming apps and components to override --calcite-button-background-color when necessary.

Example

/* button.scss */
button {
    background-color: var(--calcite-button-background-color, transparent);
}

This button's background color will be green.

/* consuming application */
calcite-button {
  --calcite-button-background-color: green;
}

Anti-pattern

This pattern should be avoided as it causes the CSS variable to become unreachable and a customer can not override it from outside the ShadowDOM.

/* button.css */
:host {
    button {
        --calcite-button-background-color: transparent;

        background-color: var(--calcite-button-background-color);
    }
}

The resulting background-color will still be transparent.

/* consuming application */
calcite-button {
  --calcite-button-background-color: green;
}

Expanded Example

Let's take it a step further. In the case of calcite-button, the web-component has several variants set by properties on the HTML or JSElement. Because these changes are only set by changes to the :host of the component, we don't need to create unique tokens for each variant. Consuming components and applications can target host variants and do their overrides as needed.

One option is to reassign the style property for every variant.

/* button.scss */
button {
    background-color: var(--calcite-button-background-color, transparent);
}

:host(:hover) {
  button {
    background-color: var(--calcite-button-background-color, var(--calcite-color-foreground-2));
  }
}

But this is a lot of code and can become difficult to maintain over time if there are a lot of variants. An alternative is to set the style property once and then use an internal token to track the internal changes based on the host variant.

/* button.scss */
button {
    background-color: var(--calcite-button-background-color, var(--calcite-internal-button-background-color, transparent));
}

:host(:hover) {
  button {
    --calcite-internal-button-background-color: var(--calcite-color-foreground-2);
  }
}

Applying sub-component tokens

The above patterns allow a consuming app or component to be able to set tokens by variant or pseudo class on the host if they needed to. In this use case, a <calcite-button> exists as a sub-component/element within a new custom component which should respond to a :hover and :active state unique from the parent component.

Example

/* my-new-component.css */
calcite-button {
  --calcite-button-background-color: red;

  &:hover {
    --calcite-button-background-color: blue;
  }
  
  &:active {
    --calcite-button-background-color: green;
  }
}

Expanded example

But in most cases, static values are an anti-pattern and instead these overrides should reference other global, semantic, and component tokens.

/* mycomponent */

calcite-button {
  --calcite-button-background-color: var(--calcite-mycomponent-button-background-color, var(--calcite-color-brand));

  &:hover {
    --calcite-button-background-color: var(--calcite-mycomponent-button-background-color-hover, var(--calcite-color-brand-hover));
  }
  &:active {
    --calcite-button-background-color: var(--calcite-mycomponent-button-background-color-press, var(--calcite-color-brand-press));
  }
}

Documenting tokens

Every component token should be documented at the top of the component CSS file using the Calcite JSDoc standard. Read more on the tokens documentation page.

Shared tokens

In some edge-cases we do not want to create unique tokens for a component and instead adhere to the design pattern defined by a parent or global token.

flowchart TD
    A1{<a target="_blank" href="https://github.com/Esri/calcite-design-system/blob/6ccb12a17ec371ba13fe144f8d37fdfb607dfce2/packages/calcite-components/stencil.config.ts#L17">Are components associated?</a>}
    A2{Can it be used independently?}
    B1{Look at the component in Figma.<br />Does it share a visual pattern<br />with associated components?}
    C1{It can share tokens}
    C2{It should have unique tokens}

    A1 --> |Yes| A2
    A1 -->|No| C2
    A2 -->|No| C1
    A2 -->|Yes| B1
    B1 -->|Yes| C1
    B1 -->|No| C2
Loading

Example

--calcite-combobox-color-background is used in both Combobox & ComboboxItem. This is documented in the JSDocs at the top of each component's SCSS file.

This is also the case for elements in the component which share a pattern. In most use-cases icons in the components should reference the -text-color token. In rare cases when Icons should not follow the text color pattern, check if it can use the -color-accent pattern. If not, add an -icon- element to the naming schema.

Development best practices

1. Make sure to branch off dev

Some components in the epic/7180-component-tokens branch are very out of date with the dev branch. This makes sure the most recent JSX, resources and utils are being used.

2. Review the Calcite UIKit in Figma for patterns

You can find the Calcite UIKit on the Esri Figma Community Profile

  • What tokens are related to this component?
  • Are there shared interaction patterns?
  • Are the use cases covered by -hover and -press?
  • Is there an indicator pattern? This is usually border-input or transparent by default and -brand, -brand-press when :active or :hovered.

3. Review component for "tokenizable" styles

The most common token styles are colors, shadow, and radius. However some components may need to have additional tokens to meet user requirements. While some components already have tokens, many do not. Use the UIKit, your own intuition, as well as feedback from the team to decide how many tokens you need to create to apply styles to the following CSS properties across the component.

  • background-color
  • border-color
  • color (text-color)
  • box-shadow (shadow)
  • border-radius (corner-radius)

Note

The custom property name does not need to match the corresponding CSS property. In the list above, the preferred name for the component token is suggested in parenthesis, allowing for more flexibility and contextual naming.

4. Update component SCSS to use CSS variable tokens

[property]: var([component-token], var([global-token/fallback]));

Sometimes it's useful to utilize -internal- domain tokens. These are not considered official Calcite Design Tokens and are for developer convenience. If a pattern is observed in -internal- tokens. Bring it up to the team for consideration to be move to a Calcite Design Token. Read more about internal tokens here.

5. Write tests

E2E

  • Use themed from common tests
  • If a token does not apply in the default context, add a new describe block
  • Use html formatter for non-default component tests

Starter template

describe("theme", () => {
  describe("default", () => {
    themed("calcite-component", {});
  });
});

Example

describe("theme", () => {
  describe("default", () => {
    themed("calcite-component", {
      // this is the name of the token
      --calcite-component-background-color: {
        // optional. This defaults to the first component in the test template.
        selector: 'calcite-component',
        // optional. The selector pattern to target a specific element within the shadow dom of the Selector component
        shadowSelector: `.${CSS.container}`
        // the CSS property you have applied a variable to. This must be written in camelCase format.
        targetProp: "backgroundColor"
      }
    });
  });
});

Storybook

  1. Find or create a component file in packages/calcite-components/src/custom-theme/[component];
  2. Use named exports and tagged template literals with html formatting to define the test component templates.
  3. Export an object of component tokens with the token name in camelCase as the key and an empty string as the value.
  4. Make sure the component templates are imported into packages/calcite-components/src/custom-theme.stories.ts and included in the kitchenSink template and the default exported args.
export const actionTokens = {
  calciteActionIndicatorColor: "",
  calciteActionBackgroundColor: "",
  calciteActionBackgroundColorHover: "",
  calciteActionBackgroundColorpress: "",
  calciteActionTextColor: "",
  calciteActionTextColorpress: "",
};

export const actionBarTokens = {
  calciteActionBarExpandedMaxWidth: "",
  calciteActionBarItemsSpace: "",
};

export const actionBar = html`<calcite-action-bar layout="horizontal" style="width:100%">
  <calcite-action-group>
    <calcite-action text="Add" icon="plus"> </calcite-action>
    <calcite-action text="Save" icon="save"> </calcite-action>
    <calcite-action text="Layers" icon="layers"> </calcite-action>
  </calcite-action-group>
  <calcite-action-group>
    <calcite-action text="Add" icon="plus"> </calcite-action>
    <calcite-action text="Save" active icon="save"> </calcite-action>
    <calcite-action text="Layers" icon="layers"> </calcite-action>
  </calcite-action-group>
  <calcite-action slot="actions-end" text="hello world" icon="layers"> </calcite-action>
  <!-- The "bottom-actions" slot is deprecated -->
  <calcite-action slot="bottom-actions" text="hello world 2" icon="information"> </calcite-action>
</calcite-action-bar>`;
import {
  actionBar,
  actionTokens,
  actionBarTokens,
} from "./custom-theme/actions";

// ...
const kitchenSink = (args: Record<string, string>, useTestValues = false) =>
  html`<div style="${customTheme(args, useTestValues)}">
    <div class="demo">
      <div>${actionBar}</div>
    </div>
  </div>
</div>`;

export default {
  title: "Theming/Custom Theme",
  args: {
    ...actionTokens,
    ...actionBarTokens,
  },
};

export const theming_TestOnly = (): string => {
  return kitchenSink(
    {
      ...actionTokens,
      ...actionBarTokens,
    },
    true,
  );
};

Demo Page

<demo-theme tokens="--calcite-tokens, --calcite-tokens-as-list">
  <!-- component HTML -->
</demo-theme>

Active development and debugging

When adding tokens, components should be fully manually visually tested. You can do this three ways.

  1. Demo pages
  2. Storybook pages
  3. Debug End2End Tests

Demo pages

npm --workspace="packages/calcite-components" run start

Used for rapid development. These pages reload quickly for easy development but do not cover all use cases for the component and therefor can not be fully relied upon for testing.

Storybook pages

npm --workspace="packages/calcite-components" run screenshot-tests:preview

Used for Chromatic tests. Before opening a PR, run storybook and manually click through the component's relevant pages to confirm no visual changes

E2E Test pages

This would run the Alert component's E2E tests in debugger mode for the alert.e2e.ts file. That means each test will be spun up in "headful" mode. Allowing you to add a debugger to the test script and freeze the test mid-run to visually confirm the component.

node_modules/@stencil/core/bin/stencil test --no-docs --no-build --no-cache --e2e --devtools -- --max-workers=0 packages/calcite-components/src/components/alert/alert.e2e.ts

If you are using VSCode this is made much simpler by utilizing the built in Calcite E2E Debug helper.

  1. Right click on "themed" to open the E2E theming helper file.
Screenshot 2024-09-16 at 10 38 42 AM
  1. Add a debugger to line :172
Screenshot 2024-09-16 at 10 39 12 AM
  1. Speed up debugging by .only running the relevant test.
Screenshot 2024-09-16 at 10 39 25 AM
  1. Run the VSCode debugger script Debug Stencil --e2e {currentFile}
Screenshot 2024-09-16 at 10 39 37 AM
  1. If you try to run the script and hit an error. Try adding debuggers to lines :114 and :142. Inspect the HTML and confirm your component has the expected elements needed.

Working Examples

Accordion

Accordion & Accordion Item are directly related and so can be done together. After reviewing the Figma file it was determined that although there were several different instances of text-color and background-color being applied, only a few actual color values were used. This was simplified down to five tokens and applied to both components.

https://github.com/Esri/calcite-design-system/pull/9861

Action

All Action components, Action, Action Bar, Action Group, Action Menu, and Action Pad share many of the same design patterns and therefor should share a lot of the base action tokens. However, several of the more complex action components have their own additional tokens on top of Action.

https://github.com/Esri/calcite-design-system/pull/10058

Icon

Icon is used in all kinds of places. Here most of the work was a quick find-replace of the old --calcite-ui-icon-color token in favor of --calcite-icon-color including in the Calcite Tailwind presets file. This also including removal of some overrides of the --calcite-icon-color token where it was unnecessary and was blocking theming when Storybook tests were run.

https://github.com/Esri/calcite-design-system/pull/10062