-
Notifications
You must be signed in to change notification settings - Fork 77
tokens component
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.
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.
- 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.
- If components are grouped in Figma they should share a design pattern. Tokens shared between these components should use the name of the top-level parent. Tokens defining only design decisions for a sub-elements of a specific component may use that specific component name.
- Any tokens used in a component should be documented in the component they effect.
- 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
. - There may be edge cases which require stateful tokens for slotted elements.
- 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
flowchart TD
add{add a new token. Confirm the naming schema with the Calcite team.}
use{use existing token}
deprecate{deprecate the variable}
pass{Sub-components accept tokens from the parent. 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]
A2[Looking at a Calcite component's Figma 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>]
C1[this value is used many times and it would be very helpful to have a single variable representing this value.]
D1[grouped components/elements share the design pattern/value]
E2[these elements already have a token]
F1[sub-components are slotted]
G1[token naming schema is out of date]
G2[component needs to override the tokens of the sub-component]
H1[slotted components are part of a component-group in Figma <em>Example: Tabs, Tab-Title, Tab-Nav, Tab</em>]
A1 --> B1
A2 --> B1
B1 -- No --> C1
B1 -- Yes --> D1
C1 -- Yes --> internal
C1 -- No --> nothing
D1 -- Yes --> F1
D1 -- No --> E2
E2 -- Yes --> G1
E2 -- No --> add
F1 -- Yes --> H1
F1 -- No --> G2
G1 -- Yes --> deprecate
G1 -- No --> use
G2 -- Yes --> add
G2 -- No --> nothing
H1 -- Yes --> pass
H1 -- No --> G2
deprecate --> add
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.
/* 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;
}
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;
}
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);
}
}
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.
/* my-new-component.css */
calcite-button {
--calcite-button-background-color: red;
&:hover {
--calcite-button-background-color: blue;
}
&:active {
--calcite-button-background-color: green;
}
}
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));
}
}
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.
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
--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.
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.
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
accent
pattern? This is usuallyborder-input
ortransparent
by default and-brand
,-brand-press
when:active
or:hovered
.
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.
[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.
- 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"
}
});
});
});
- Find or create a component file in
packages/calcite-components/src/custom-theme/[component]
; - Use named exports and tagged template literals with
html
formatting to define the test component templates. - Export an object of component tokens with the token name in camelCase as the key and an empty string as the value.
- Make sure the component templates are imported into
packages/calcite-components/src/custom-theme.stories.ts
and included in thekitchenSink
template and the default exportedargs
.
export const actionTokens = {
calciteAccentColorPress: "",
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-theme tokens="--calcite-tokens, --calcite-tokens-as-list">
<!-- component HTML -->
</demo-theme>
When adding tokens, components should be fully manually visually tested. You can do this three ways.
- Demo pages
- Storybook pages
- Debug End2End Tests
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.
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
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.
- Right click on "themed" to open the E2E theming helper file.
- Add a debugger to line
:172
- Speed up debugging by
.only
running the relevant test.
- Run the VSCode debugger script
Debug Stencil --e2e {currentFile}
- 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.
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
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 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.