- Introduction
- Types of tokens and how to generate them
- One step further - Context and Roles
- Interaction states
- Naming
This exercise serves as a proof of concept, aiming to emulate as faithfully as possible the design token generation process utilized by the Material Design team, which relies on the Sass preprocessor. It employs a purely JavaScript-based approach and consumes the tokens in the browser in real-time.
- Reference token
- System token
- Component token
Diagram of a button that receives its container color through a system of three tokens that define scalable color values. The color tokens point to a specific hex value that can easily change without impacting the token syntax.
- Reference tokens: md-ref-palette.scss
- System tokens: md-sys-color.scss
- Component tokens:
SASS Version
- @mixin styles(), using @function values(), generates in the
:host
selector the necessary CSS private variables, using the "System Tokens" values. - @include [name-component.]theme() assigns values to the "Component Tokens" of another component using the previously created values.
@function values() points to v0_192, this version may change, and the link could break.
- Reference tokens: blk-ref-palette.js
- System tokens: blk-sys-color.js
- Component tokens: blk-button-tokens.js
JS Version
- setTokens() generates in the
:host
selector the necessary CSS private variables, using the "System Tokens" object. - setVariables() assigns values to the "Component Tokens" of another component using the previously created values.
- styleTokens() joins the generated CSS variable with a CSS selector, such as
:host
.
Both aim to "generate" a CSS variable for the Component Token and another for the system token with a fallback to the reference value, and provide the possibility to modify both the component token and the System Token.
Material
:host {
--_container-color: var(--md-filled-button-container-color, var(--md-sys-color-primary, #6750a4));
}
The entire process of generating tokens is always divided into two parts:
- Creating the necessary CSS variables
- Creating the necessary CSS selectors to apply those variables
import { PREFIX as blkRipplePrefix } from './blk-ripple-tokens.js';
// The { Object } name "identifies" a set of system tokens. - a.k.a sysColor
// The Key "identifies" the private CSS variable and the name for the Component Token.
// The Value "identifies" a specific token in the system token set.
const sysColor = {
'container-color': 'primary',
/* blkRipple */
'hover-state-layer-color': 'on-primary',
};
// 1. setTokens - Creating the necessary CSS variables
const systemTokens = { sysColor, sysTypescale, };
const tokens = setTokens(systemTokens, PREFIX); // const PREFIX = 'blk-button';
// The Key "identifies" the Component Token of another component.
// The Value "identifies" the private CSS variable and the name for the component token.
const blkRipple = {
'hover-color': 'hover-state-layer-color',
};
// 1. setVariables - Assigns values to Component Token of another component. - blk-ripple-tokens.js
const themeVars = setVariables(blkRipple, blkRipplePrefix); // PREFIX as blkRipplePrefix = 'blk-ripple';
// 2. styleTokens - Creating the necessary CSS selectors to apply those variables
export const styleTokens = css`
${cssStyleRule(`:host`, [tokens, themeVars])}
`;
setTokens
returns
--_container-color: var(--blk-button-container-color, var(--blk-sys-color-primary, #6750a4));
--_hover-state-layer-color: var(--blk-button-hover-state-layer-color, var(--blk-sys-color-on-primary, #fff));
--blk-ripple-hover-color: var(--_hover-state-layer-color);
styleTokens
returns
:host {
--_container-color: var(
--blk-button-container-color, /* component token */
var(--blk-sys-color-primary, #6750a4) /* system token */
);
--_hover-state-layer-color: var(
--blk-button-hover-state-layer-color, /* component token */
var(--blk-sys-color-on-primary, #fff) /* system token */
);
--blk-ripple-hover-color: var(--_hover-state-layer-color); /* component token of another component */
}
blk-button.scss
button:{
&::before {
background-color: var(--_container-color);
border-radius: inherit;
...
}
}
BlkButton.js
import { LitElement, html, unsafeCSS } from 'lit';
import { styleTokens } from './styles/blk-button-tokens.js';
import { styles } from './styles/blk-button-styles.css.js';
...
export class BlkButton extends LitElement {
static styles = [unsafeCSS(styleTokens), styles];
...
}
Tokens can point to different values depending on a set of conditions. These conditions are called “Contexts” and the override values are called contextual values.
The same system token for background color can point to different reference tokens depending on light or dark theme contexts
But “Contexts” is intended to be used with “system tokens” and @media CSS. Media queries allow you to apply CSS styles based on a device's general type, such as print versus screen, or other characteristics
In addition to color changes based on Context
, a component might want to alter its display based on its Contained Areas
.
Surface colors define contained areas, distinguishing them from a background and other on-screen elements.
There are three core surface roles:
- surface dim
- surface
- surface bright
In addition to five surface container roles:
- surface container lowest
- surface container low
- surface container
- surface container high
- surface container highest
As mentioned in Setting Tokens like CSS custom properties the foundation involves creating a CSS variable that "reflects" the component token and another that "reflects" the system token.
setTokens() also creates a variable intended to "receive roles values".
The component that "exposes" these values is responsible for providing them with the necessary value.
--_container-color: var(
--blk-button-container-color,
var(
--blk-button-roles-container-color, /* roles values */
var(--blk-button-initial-container-color, var(--blk-sys-color-primary, #6750a4))
)
);
Similar to the other tokens, the process is always divided into two parts:
- creating the necessary CSS variables
- generating the required CSS selectors to apply those variables.
// The { Object } name "identifies" a contained areas roles
// The Key "identifies" the "roles value" CSS variable.
// The Value "identifies" a specific token in the system token set.
const dim = {
'container-color': 'secondary',
};
const surface = {
'container-color': 'primary',
};
const bright = {
'container-color': 'tertiary',
};
// 1. setRoles - Creating the necessary CSS variables
const systemRoles = { dim, surface, bright };
const roles = setRoles(systemRoles, PREFIX); // const PREFIX = 'blk-button';
// 2. styleRoles - Creating the necessary CSS selectors to apply those variables (roles ralues)
export const styleRoles = Object.entries(roles)
.map(([role, value]) => cssStyleRule(`:host([${ROLES}="${role}"])`, [value]))
.join('');
// Join all tokens styles
export const styleTokens = `
${cssStyleRule(`:host`, [tokens, themeVars])}
${styleRoles}
`;
- styleTokens returns
...
:host([roles='dim']) {
--blk-button-roles-container-color: var(
--blk-button-roles-dim-container-color, /* new component token for roles values */
var(--blk-sys-color-secondary, #625b71)
);
}
:host([roles='surface']) {
--blk-button-roles-container-color: var(
--blk-button-roles-surface-container-color, /* new component token for roles values */
var(--blk-sys-color-primary, #6750a4)
);
}
:host([roles='bright']) {
--blk-button-roles-container-color: var(
--blk-button-roles-bright-container-color, /* new component token for roles values */
var(--blk-sys-color-tertiary, #7d5260)
);
}
The function
setRoles()
also generates a "Component Token" associated with "role value" to be able to modify if necessary.
--blk-button-roles-container-color: var(
--blk-button-roles-bright-container-color, /* new component token for roles values */
var(--blk-sys-color-tertiary, #7d5260)
);
When generating tokens, a mild CSS Variable is created, and it will always have the lowest specificity compared to the rest of the "component tokens / roles values" It can be used with the awareness that when a selector for role values is used, it will be overwritten. Alternatively, if role values are not used and there's a need to override a value while still keeping the component token variable available, the initial CSS variable can be used.
--_container-color: var(
--blk-button-container-color,
var(
--blk-button-roles-container-color,
var(
--blk-button-initial-container-color, /* initial `mild` value */
var(--blk-sys-color-primary, #6750a4)
)
)
);
https://m3.material.io/foundations/interaction/states/applying-states
An activated
state differs from a selected
state because it communicates a highlighted destination.
States can be combined with onto-states, such as:
enabled/default
andhover
selected
andfocused
activated
andpressed
- Enabled
- Disabled
- Selected
- Unselected
- Activated
- Hover
- Focused
- Pressed
- Dragged
[component-token]-[variant?]-[state?]-[onto-state?]-[element?]-[css-property]
- --md-primary-tab-[variant?]-[activated]-label-text-color
- --md-primary-tab-[variant?]-[activated]-[hover]-label-text-color
- --md-primary-tab-[variant?]-[activated]-[focused]-label-text-color
- --md-primary-tab-[variant?]-[activated]-[pressed]-label-text-color
- --md-primary-tab-['positve']-[activated]-label-text-color
- --md-primary-tab-['positve']-[activated]-[hover]-label-text-color
- --md-primary-tab-['positve']-[activated]-[focused]-label-text-color
- --md-primary-tab-['positve']-[activated]-[pressed]-label-text-color