Skip to content

Design Tokens

huwilkes edited this page Jul 7, 2023 · 10 revisions

Overview

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.

Tokens

Design tokens come in three tiers.

  • Global tokens are effectively friendly names for constants. These have no semantic meaning on their own.
    • Example: grey 64 = #A3A3A3, medium icon size = 24 pt
  • 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
  • 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 Swift representation of tokens will reflect his hierarchy, with a unidirectional dependency hierarchy. Controls should only depend on Control tokens, without ever directly referring to an alias or global token.

APIs

Global tokens

Let’s start with the basic definition of our global tokens. Below is a sample subset of various types of global tokens.

public final class GlobalTokens {

    // MARK: - NeutralColor

    @objc(MSFGlobalTokensNeutralColor)
    public enum NeutralColorToken: Int, TokenSetKey {
        case black
        case grey2
        case grey4
        case grey6
        // ...
        case grey94
        case grey96
        case grey98
        case white
    }
    @objc(colorForNeutralColorToken:)
    public static func neutralColor(_ token: NeutralColorToken) -> UIColor {
        switch token {
        case .black:
            return UIColor(hexValue: 0x000000)
        case .grey2:
            return UIColor(hexValue: 0x050505)
        case .grey4:
            return UIColor(hexValue: 0x0A0A0A)
        case .grey6:
            return UIColor(hexValue: 0x0F0F0F)
        // ...
        case .grey94:
            return UIColor(hexValue: 0xF0F0F0)
        case .grey96:
            return UIColor(hexValue: 0xF5F5F5)
        case .grey98:
            return UIColor(hexValue: 0xFAFAFA)
        case .white:
            return UIColor(hexValue: 0xFFFFFF)
        }
    }

    // MARK: - BorderRadius

    @objc(MSFGlobalTokensCornerRadius)
    public enum CornerRadiusToken: TokenSetKey {
        case radiusNone
        case radius20
        case radius40
        case radius60
        case radius80
        case radius120
        case radiusCircular
    }
    @objc(cornerForToken:)
    public static func corner(_ token: CornerRadiusToken) -> CGFloat {
        switch token {
        case .radiusNone:
            return 0
        case .radius20:
            return 2
        case .radius40:
            return 4
        case .radius60:
            return 6
        case .radius80:
            return 8
        case .radius120:
            return 12
        case .radiusCircular:
            return 9999
        }
    }
}

These read-only tokens can be accessed via both Swift and Objective-C using the provided static lookup functions.

Alias Tokens

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.

These values are stored on a category of FluentTheme so they can be easily accessed from any theme object.

public extension FluentTheme {
    @objc(MSFColorToken)
    enum ColorToken: Int, TokenSetKey {
        // Neutral colors - Background
        case background1
        case background1Pressed
        case background1Selected
        case background2
        case background2Pressed
        case background2Selected

        // ...
    }

    /// Returns the color value for the given token.
    ///
    /// - Parameter token: The `ColorsTokens` value to be retrieved.
    /// - Returns: A `UIColor` for the given token.
    @objc(colorForToken:)
    func color(_ token: ColorToken) -> UIColor {
        switch token {
        case .background1:
            return UIColor(light: GlobalTokens.neutralColor(.white),
                           dark: GlobalTokens.neutralColor(.black),
                           darkElevated: GlobalTokens.neutralColor(.grey4))
        case .background1Pressed:
            return UIColor(light: GlobalTokens.neutralColor(.grey88),
                           dark: GlobalTokens.neutralColor(.grey18),
                           darkElevated: GlobalTokens.neutralColor(.grey18))
        case .background1Selected:
            return UIColor(light: GlobalTokens.neutralColor(.grey92),
                           dark: GlobalTokens.neutralColor(.grey14),
                           darkElevated: GlobalTokens.neutralColor(.grey14))
        case .background2:
            return UIColor(light: GlobalTokens.neutralColor(.white),
                           dark: GlobalTokens.neutralColor(.grey12),
                           darkElevated: GlobalTokens.neutralColor(.grey16))
        case .background2Pressed:
            return UIColor(light: GlobalTokens.neutralColor(.grey88),
                           dark: GlobalTokens.neutralColor(.grey30),
                           darkElevated: GlobalTokens.neutralColor(.grey30))
        case .background2Selected:
            return UIColor(light: GlobalTokens.neutralColor(.grey92),
                           dark: GlobalTokens.neutralColor(.grey26),
                           darkElevated: GlobalTokens.neutralColor(.grey26))

        // ...
        }
    }
}

Control tokens

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 subclass and provide custom values for these tokens. Custom Control tokens can be provided for any control either upon instantiation or after presentation via a public property.

open class CardNudgeTokens: ControlTokens {
    open var accentColor: ColorSet {
        return globalTokens.brandColors[.shade20]
    }

    open var accentIconSize: CGFloat {
        return globalTokens.iconSize[.xxSmall]
    }
}

Sub-Control tokens

Tokens for controls contained within another control should be defined in the hosting control’s tokens as optional, then passed down as a custom subclass of the contained control’s tokens to the contained control through override tokens.

open class ListTokens: ControlTokens {
    open var dividerColor: DynamicColor? { nil }
}
private class CustomDividerTokens: DividerTokens {
    var listTokens: ListTokens
    init (listTokens: ListTokens) {
        self.listTokens = listTokens
        super.init()
    }
    override var color: DynamicColor {
        listTokens.dividerColor ?? super.color
    }
}

FluentTheme

As an app, the first step towards providing a custom Fluent UI appearance for your app is to subclass the specific Control theme object(s) that you wish to override. Want to change button corner radii? Subclass 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—global, alias, and control—and apply them holistically to a specific view hierarchy or window.

For more details, see Overriding Tokens.

SwiftUI

Our framework provides a simple ViewModifier called fluentTheme(_:) which takes a FluentTheme instance as its only argument. Simply add this view modifier to the highest point in your view hierarchy in which you’d like the custom theme to apply, and we’ll take care of the rest.

public var body: some View {
    VStack {
        // ...
        // some views that contain Fluent components
        // ...
    }
    .fluentTheme(myCustomFluentTheme)
}

If you'd like to use tokens from the FluentTheme in the environment for your own controls, you can retrieve the FluentTheme from the environment via EnvironmentValues by adding @Environment(\.fluentTheme) var fluentTheme: FluentTheme to your view.

UIKit

Custom themes can be set via a public extension on UIView. Simply set the fluentTheme property on the view or window you wish to host your components in, and once again we’ll take care of the rest.

See below for a rough idea of how it’s implemented internally:

protocol FluentThemeable {
    var fluentTheme: FluentTheme? { get set }
}
 
extension UIView: FluentThemeable {
    private struct Keys {
        static var fluentTheme: String = "fluentTheme_key"
    }

    /// The custom `FluentTheme` to apply to this window.
    public var fluentTheme: FluentTheme? {
        get {
            return objc_getAssociatedObject(self, &Keys.fluentTheme) as? FluentTheme
        }
        set {
            objc_setAssociatedObject(self, &Keys.fluentTheme, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            NotificationCenter.default.post(name: .didChangeTheme, object: nil)
        }
    }
}
Clone this wiki locally