-
Notifications
You must be signed in to change notification settings - Fork 108
Design Tokens
This document covers the basics of our design token system and how it is implemented internally. For details of how to customize tokens in your component or app, see Overriding Tokens.
Design tokens come in three tiers.
- Global tokens are effectively friendly names for constants. These have no semantic meaning on their own. They are not override-able.
- Example:
grey 64 = #A3A3A3
,medium icon size = 24 pt
- Example:
- Alias tokens are imbued with semantic meaning. They should be derived from global tokens. Color-based alias tokens will have a number variants for different contexts (e.g. light mode, dark mode, high contrast, etc) and may vary from endpoint to endpoint.
- Example:
neutral background 2 = gray 98 @ light, gray 36 @ dark
- Example:
- Control tokens define how a given control draws itself using alias tokens. This is most likely to be defined in a platform-specific manner.
- Example:
button background = neutral background 2
Our Android representation of tokens will reflect its hierarchy, with a unidirectional dependency hierarchy. Controls should only depend on Control tokens, without ever directly referring to an alias or global token.
- Example:
Let’s start with the basic definition of our global tokens. Below is a sample subset of various types of global tokens.
@Parcelize
object class GlobalTokens : Parcelable {
enum class NeutralColorTokens {
Black,
Grey2,
Grey4,
Grey6,
Grey8,
Grey10,
Grey12,
Grey14,
Grey16,
Grey18,
Grey20,
Grey22,
Grey24,
Grey26,
Grey28,
Grey30
}
fun neutralColor(token: NeutralColorTokens): Color {
return when (token) {
NeutralColorTokens.Black -> Color(0xFF000000)
NeutralColorTokens.Grey2 -> Color(0xFF050505)
NeutralColorTokens.Grey4 -> Color(0xFF0A0A0A)
NeutralColorTokens.Grey6 -> Color(0xFF0F0F0F)
NeutralColorTokens.Grey8 -> Color(0xFF141414)
NeutralColorTokens.Grey10 -> Color(0xFF1A1A1A)
NeutralColorTokens.Grey12 -> Color(0xFF1F1F1F)
NeutralColorTokens.Grey14 -> Color(0xFF242424)
NeutralColorTokens.Grey16 -> Color(0xFF292929)
NeutralColorTokens.Grey18 -> Color(0xFF2E2E2E)
NeutralColorTokens.Grey20 -> Color(0xFF333333)
NeutralColorTokens.Grey22 -> Color(0xFF383838)
NeutralColorTokens.Grey24 -> Color(0xFF3D3D3D)
NeutralColorTokens.Grey26 -> Color(0xFF424242)
NeutralColorTokens.Grey28 -> Color(0xFF474747)
NeutralColorTokens.Grey30 -> Color(0xFF4D4D4D)
}
}
enum class ShadowTokens {
Shadow02,
Shadow04,
Shadow08,
Shadow16,
Shadow28,
Shadow64,
}
fun elevation(token: ShadowTokens): Dp {
return when (token) {
ShadowTokens.Shadow02 -> 2.dp
ShadowTokens.Shadow04 -> 4.dp
ShadowTokens.Shadow08 -> 8.dp
ShadowTokens.Shadow16 -> 16.dp
ShadowTokens.Shadow28 -> 28.dp
ShadowTokens.Shadow64 -> 40.dp
}
}
}
These tokens cannot be read or modified and are created by the fluent design team with careful consideration to be used as design standards.
Alias tokens are similar to global tokens, with one large change: they should reference global tokens whenever possible. Their goal is to provide semantic meaning for the raw values defined in the global system.
open class AliasTokens {
protected var globalTokens: GlobalTokens = GlobalTokens()
@Composable
open fun updateGlobalToken() {
this.globalTokens = FluentTheme.globalTokens
}
enum class NeutralBackgroundColorTokens {
Background1,
Background1Pressed,
Background1Selected,
Background2
}
open val neutralBackgroundColor: TokenSet<NeutralBackgroundColorTokens, FluentColor> by lazy {
TokenSet { token ->
when (token) {
NeutralBackgroundColorTokens.Background1 ->
FluentColor(
light = GlobalTokens.neutralColor(GlobalTokens.NeutralColorTokens.White),
dark = GlobalTokens.neutralColor(GlobalTokens.NeutralColorTokens.Black)
)
NeutralBackgroundColorTokens.Background1Pressed ->
FluentColor(
light = GlobalTokens.neutralColor(GlobalTokens.NeutralColorTokens.Grey88),
dark = GlobalTokens.neutralColor(GlobalTokens.NeutralColorTokens.Grey18)
)
NeutralBackgroundColorTokens.Background1Selected ->
FluentColor(
light = GlobalTokens.neutralColor(GlobalTokens.NeutralColorTokens.Grey92),
dark = GlobalTokens.neutralColor(GlobalTokens.NeutralColorTokens.Grey14)
)
NeutralBackgroundColorTokens.Background2 ->
FluentColor(
light = GlobalTokens.neutralColor(GlobalTokens.NeutralColorTokens.White),
dark = GlobalTokens.neutralColor(GlobalTokens.NeutralColorTokens.Grey12)
)
}
}
}
Control token sets are simply the collection of tokens used by a single control. These should utilize Alias tokens whenever possible, though it may be necessary to reference a global token in cases where no such alias exists.
A default implementation of the component’s theme will exist alongside the component, providing clients the opportunity to inherit and override custom values for these tokens. Custom Control tokens can be provided for any control either upon instantiation or after presentation via a public property.
@Parcelize
open class ButtonTokens : ControlToken, Parcelable {
companion object {
const val Type: String = "Button"
}
@Composable
open fun iconColor(buttonInfo: ButtonInfo): StateColor {
return when (buttonInfo.style) {
ButtonStyle.Button ->
StateColor(
rest = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
themeMode = themeMode
),
pressed = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
themeMode = themeMode
),
selected = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
themeMode = themeMode
),
focused = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
themeMode = themeMode
),
disabled = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
themeMode = themeMode
)
)
ButtonStyle.OutlinedButton, ButtonStyle.TextButton ->
StateColor(
rest = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
themeMode = themeMode
),
pressed = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForeground1Pressed].value(
themeMode = themeMode
),
selected = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForeground1Selected].value(
themeMode = themeMode
),
focused = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
themeMode = themeMode
),
disabled = aliasTokens.neutralForegroundColor[AliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
themeMode = themeMode
)
)
}
}
}
As an app, the first step towards providing a custom Fluent UI appearance for your app is to extend and override the specific Control theme object(s) that you wish to override. Want to change button corner radii? Extend ButtonTokens and provide an alternate value. Once your custom tokens are available, they can be directly set on a control instance via a public property.
However, there is another way to set properties “globally”: FluentTheme
.
FluentTheme
represent a way to group together a custom set of tokens— alias and control—and apply them holistically to a specific view hierarchy, window, or even the entire app.
A custom instance of AliasTokens can be used to initialize a FluentTheme:
FluentTheme(aliasTokens = MyAliasTokens()) {
// Your code here
}
The FluentTheme wraps around the content to be created and provides updated values to the controls. If no relevant tokens can be found, then we will fall back to the FluentUI default values.
For more details, see Overriding Tokens.
Note: Please go through Android's interoperability documentation to understand how to use XML based layouts and UI with Jetpack Compose and vice versa.