Design Tokens for Fluent UI Web Components
Fluent UI Web Components provides first-class support for Design Tokens and makes setting, getting, and using Design Tokens simple.
What is a Design Token
A Design Token is a semantic, named variable used to describe a Design System. They often describe design concepts like typography, color, sizes, UI spacing, etc. Fluent UI encourages checking out the Design Tokens Community Group for more information on Design Tokens themselves.
Fluent UI Design Tokens
The @fluentui/web-components
have extensive support for predefined design tokens. See styling the components for details on adjusting or using the existing tokens, or read on to create your own design tokens.
Create a Token
Note
This example uses color because it's an easy concept to describe, but we generally discourage the use of fixed colors as they don't benefit from the adaptive color system with support for light and dark mode and other adjustments.
The first step to using a token is to create it:
import { DesignToken } from '@microsoft/fast-foundation';
export const specialColor = DesignToken.create<string>('special-color');
Note
The Fluent UI Web Components are built using Microsoft's fast-foundation
and fast-element
libraries, so customizing requires using the original FAST types that underlie the Fluent UI Web Components.
The type assertion informs what types the token can be set to (and what type will be retrieved), and the name parameter will serve as the CSS Custom Property name (more on that later).
Setting Values
A DesignToken
value is set for a FASTElement
or HTMLBodyElement
node. This allows tokens to be set to different values for distinct DOM trees:
const ancestor = document.querySelector('my-element') as FASTElement;
const descendent = ancestor.querySelector('my-element') as FASTElement;
specialColor.setValueFor(ancestor, '#FFFFFF');
specialColor.setValueFor(descendent, '#F7F7F7');
Setting a Default Value
A default value can be set for a token, so that the default value is returned from getValueFor()
in cases where no other token value is found for a node tree.
specialColor.withDefault('#FFFFFF');
Getting Values
Once the value is set for a node, the value is available to use for that node or any descendent node. The value returned will be the value set for the nearest ancestor (or the element itself).
specialColor.getValueFor(ancestor); // "#FFFFFF"
specialColor.getValueFor(descendent); // "#F7F7F7"
Deleting Values
Values can be deleted for a node. Doing so causes retrieval of the nearest ancestor's value instead:
specialColor.deleteValueFor(descendent);
specialColor.getValueFor(descendent); // "#FFFFFF"
CSS Custom Property emission
Unless configured not to, a DesignToken emits a token to CSS automatically whenever the value is set for an element. In the case when a DesignToken is assigned a derived value, the CSS custom property will also be emitted when any dependent tokens change.
A DesignToken can be configured not to emit to a CSS custom property by passing a configuration with cssCustomPropertyName
set to null
during creation:
DesignToken.create<number>({
name: 'my-token',
cssCustomPropertyName: null,
});
A DesignToken can also be configured to emit to a CSS custom property that is different than the provided name by providing a CSS custom property name to the configuration:
DesignToken.create<number>({
name: 'my-token',
cssCustomPropertyName: 'my-css-custom-property-name', // Emits to --my-css-custom-property-name
});
Values with a 'createCSS' method
It is sometimes useful to be able to set a token to a complex object but still use that value in CSS. If a DesignToken is assigned a value with a createCSS
method on it, the product of that method will be used when emitting to a CSS custom property instead of the Design Token value itself:
interface RGBColor {
r: number;
g: number;
b: number;
createCSS(): string;
}
const extraSpecialColor = DesignToken.create<RGBColor>('extra-special-color');
const value = {
r: 255,
g: 0,
b: 0,
createCSS() {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
},
};
extraSpecialColor.setValueFor(descendent, value);
Subscription
DesignToken
supports subscription, notifying a subscriber when a value changes. Subscriptions can subscribe to any change throughout the document tree or they can subscribe changes for specific elements.
Example: Subscribe to changes for any element
const subscriber = {
handleChange(record) {
console.log(`DesignToken ${record.token} changed for element ${record.target}`);
},
};
specialColor.subscribe(subscriber);
Example: Subscribe to changes a specific element
// ...
const target = document.body.querySelector('#my-element');
specialColor.subscribe(subscriber, target);
Subscribers can be unsubscribed using the unsubscribe()
method:
// ...
specialColor.unsubscribe(subscriber);
specialColor.unsubscribe(subscriber, target);
Using Design Tokens in CSS
Any token can be used directly in a stylesheet by using the Design Token as a CSS directive. Assuming the token value has been set for the element or some ancestor element, the value of the token embedded in the stylesheet will be the token value for that element instance.
import { css } from '@microsoft/fast-element';
const styles = css`
:host {
background: ${specialColor};
}
`;
At runtime, the directive is replaced with a CSS custom property, and the Directive ensures that the CSS custom property is added for the element.
Derived Design Token Values
In the examples above, the design token is always being set to a simple string value. But, we can also set a Design Token to be a function that derives a value. A derived value receives the target element as its only argument and must return a value with a type matching the Design Token:
const token = DesignToken.create<number>('token');
token.setValueFor(target, element => 12);
The above example is contrived, but the target element can be used to retrieve other Design Tokens:
Example: A derived token value that uses another design token
const foregroundColor = DesignToken.create<string>('foreground-color');
foregroundColor.setValueFor(target, element =>
specialColor.getValueFor(element) === '#FFFFFF' ? '#2B2B2B' : '#262626',
);
For derived Design Token values, any change to dependent tokens will force the derived value to update (and update the CSS custom property if applicable). The same is true if an observable property is used by the derived value:
import { observable } from '@microsoft/fast-element';
class ThemeManager {
@observable
theme: 'blue' | 'red' = 'blue';
}
const themeManager = new ThemeManager();
specialColor.setValueFor(target, () => (themeManager.theme === 'blue' ? '#0000FF' : '#FF0000'));
themeManager.theme = 'red'; // Forces the derived tokens to re-evaluate and CSS custom properties to update if applicable
Aliasing Design Tokens
In some design systems, Design Tokens may have complex hierarchies with tokens referencing other tokens. This can be accomplished by setting a Design Token to another Design Token.
const specialColor = DesignToken.create<string>('special-color');
const buttonSpecialColor = DesignToken.create<string>('button-special-color');
specialColor.setValueFor(target, '#EDEDED');
buttonSpecialColor.setValueFor(target, specialColor);
buttonSpecialColor.getValueFor(target); // "#EDEDED"