A Design Tokens Workflow (part 9)
Implementing Multi-Brand Theming with Style Dictionary
- Getting Started With Style Dictionary
- Outputting to Different Formats with Style Dictionary
- Beyond JSON: Exploring File Formats for Design Tokens
- Converting Tokens with Style Dictionary
- Organising Outputs with Style Dictionary
- Layers, referencing tokens in Style Dictionary
- Implementing Light and Dark Mode with Style Dictionary
- Implementing Light and Dark Mode with Style Dictionary (part 2)
- Implementing Multi-Brand Theming with Style Dictionary – You are here
- Creating Multiple Themes with Style Dictionary
On This Page:
Businesses often juggle multiple brand identities to connect with a variety of audiences. This approach can translate into distinct visual elements (colors, typography, and other styles) that uniquely represent a brand. A robust design system is essential to juggle these brands styles while ensuring a seamless experience across all platforms. In the last couple of articles in this series, we explored how to implement light and dark modes using design tokens and strategies for managing and organising these tokens effectively. This article delves into the world of multi-brand theming, a critical approach that enables organisations to maintain cohesive design frameworks while embracing brand diversity.
A quick recap
For this article, we’re going to make use of two layers of design tokens to help with keeping our brands consistent when the output is being used. We will use base tokens and semantic tokens.
Base Tokens
Base tokens represent the raw, literal values that form the foundation of your design system. They can include:
- Specific hex values for colours (e.g., #BA1A2A, #006BC9)
- Precise measurements for spacing (e.g., 4px, 16px)
- Font families and weights (e.g., 'Roboto', 700) Base tokens are typically named descriptively to reflect their inherent properties rather than their usage:
{"color": {"blue500": {"$value": "#007BFF","$type": "color"},"gray100": {"$value": "#F5F5F5","$type": "color"}},"size": {"4": {"$value": "4px","$type": "dimension"},"16": {"$value": "16px","$type": "dimension"}}}
Semantic Tokens
Semantic tokens add meaning and context by mapping to base tokens. They can describe the purpose or usage of a token rather than its visual properties:
{"color": {"background": {"primary": {"$value": "{color.blue500}","$type": "color"},"secondary": {"$value": "{color.gray100}","$type": "color"}}},"spacing": {"small": {"$value": "{size.4}","$type": "dimension"},"medium": {"$value": "{size.16}","$type": "dimension"}}}
By using semantic tokens, teams can update the underlying base token (e.g., changing blue500 from #0066CC to #276AB3) without needing to change references throughout the codebase.
Setting Up Token Layers for Brands
When implementing multiple brands, each brand requires its own set of base tokens that semantic tokens can reference. This allows you to maintain consistent semantic naming while varying the visual presentation across the brands. Let's explore how to organise tokens for a Design System for three distinct brands:
Brand-Specific Base Tokens
For this example, we'll create dedicated sets of base tokens for each of WWE's iconic brands: Raw, SmackDown, and NXT. These tokens will encompass essential design elements such as colours, typography, and spacing, ensuring each brand maintains a unique identity while supporting a cohesive design system. tokens/base/raw/colors.tokens
{"color": {"base": {"primary": {"$value": "#d7182a","$type": "color"},"secondary": {"$value": "#A3A3A3","$type": "color"},"background": {"$value": "#F5F5F5","$type": "color"},"text": {"$value": "#1C1C1C","$type": "color"},"accent": {"$value": "#FFD700","$type": "color"}}}}
tokens/base/raw/typography.tokens
{"font": {"family": {"primary": {"$value": "'Roboto', sans-serif","$type": "fontFamily"},"secondary": {"$value": "'Open Sans', sans-serif","$type": "fontFamily"}},"weight": {"regular": {"$value": "400","$type": "fontWeight"},"bold": {"$value": "700","$type": "fontWeight"}},"size": {"small": {"$value": "14px","$type": "dimension"},"medium": {"$value": "16px","$type": "dimension"},"large": {"$value": "24px","$type": "dimension"}}}}
tokens/base/raw/spacing.tokens
{"spacing": {"base": {"xs": {"$value": "4px","$type": "dimension"},"sm": {"$value": "8px","$type": "dimension"},"md": {"$value": "16px","$type": "dimension"},"lg": {"$value": "24px","$type": "dimension"},"xl": {"$value": "32px","$type": "dimension"}}}}
Now, let's define similar token files for Smackdown and NXT, each with their unique values: tokens/base/smackdown/colors.tokens
{"color": {"base": {"primary": {"$value": "#9933CC","$type": "color"},"secondary": {"$value": "#495057","$type": "color"},"background": {"$value": "#F8F9FA","$type": "color"},"text": {"$value": "#343A40","$type": "color"},"accent": {"$value": "#FF5722","$type": "color"}}}}
tokens/base/smackdown/typography.tokens
{"font": {"family": {"primary": {"$value": "'Montserrat', sans-serif","$type": "fontFamily"},"secondary": {"$value": "'Lato', sans-serif","$type": "fontFamily"}},"weight": {"regular": {"$value": "400","$type": "fontWeight"},"bold": {"$value": "600","$type": "fontWeight"}},"size": {"small": {"$value": "12px","$type": "dimension"},"medium": {"$value": "15px","$type": "dimension"},"large": {"$value": "22px","$type": "dimension"}}}}
tokens/base/smackdown/spacing.tokens
{"spacing": {"base": {"xs": {"$value": "2px","$type": "dimension"},"sm": {"$value": "6px","$type": "dimension"},"md": {"$value": "12px","$type": "dimension"},"lg": {"$value": "20px","$type": "dimension"},"xl": {"$value": "28px","$type": "dimension"}}}}
tokens/base/nxt/colors.tokens
{"color": {"base": {"primary": {"$value": "#2E7D32","$type": "color"},"secondary": {"$value": "#546E7A","$type": "color"},"background": {"$value": "#E8F5E9","$type": "color"},"text": {"$value": "#1B5E20","$type": "color"},"accent": {"$value": "#F57F17","$type": "color"}}}}
tokens/base/nxt/typography.tokens
{"font": {"family": {"primary": {"$value": "'Poppins', sans-serif","$type": "fontFamily"},"secondary": {"$value": "'Merriweather', serif","$type": "fontFamily"}},"weight": {"regular": {"$value": "300","$type": "fontWeight"},"bold": {"$value": "500","$type": "fontWeight"}},"size": {"small": {"$value": "16px","$type": "dimension"},"medium": {"$value": "18px","$type": "dimension"},"large": {"$value": "26px","$type": "dimension"}}}}
tokens/base/nxt/spacing.tokens
{"spacing": {"base": {"xs": {"$value": "5px","$type": "dimension"},"sm": {"$value": "10px","$type": "dimension"},"md": {"$value": "20px","$type": "dimension"},"lg": {"$value": "30px","$type": "dimension"},"xl": {"$value": "40px","$type": "dimension"}}}}
Shared Semantic Tokens
The key advantage of this approach is that semantic tokens can remain consistent across all themes, using a single shared naming convention with the ds-
prefix:
tokens/semantic/colors.tokens
{"color": {"background": {"default": {"$value": "{color.base.background}","$type": "color"}},"text": {"default": {"$value": "{color.base.text}","$type": "color"}},"action": {"primary": {"$value": "{color.base.primary}","$type": "color"},"secondary": {"$value": "{color.base.secondary}","$type": "color"},"highlight": {"$value": "{color.base.accent}","$type": "color"}}}}
tokens/semantic/typography.tokens
{"typography": {"heading": {"fontFamily": {"$value": "{font.family.primary}","$type": "fontFamily"},"fontWeight": {"$value": "{font.weight.bold}","$type": "fontWeight"},"fontSize": {"$value": "{font.size.large}","$type": "dimension"}},"body": {"fontFamily": {"$value": "{font.family.secondary}","$type": "fontFamily"},"fontWeight": {"$value": "{font.weight.regular}","$type": "fontWeight"},"fontSize": {"$value": "{font.size.medium}","$type": "dimension"}},"caption": {"fontFamily": {"$value": "{font.family.secondary}","$type": "fontFamily"},"fontWeight": {"$value": "{font.weight.regular}","$type": "fontWeight"},"fontSize": {"$value": "{font.size.small}","$type": "dimension"}}}}
tokens/semantic/spacing.tokens
{"spacing": {"element": {"xsmall": {"$value": "{spacing.base.xs}","$type": "dimension"},"small": {"$value": "{spacing.base.sm}","$type": "dimension"},"medium": {"$value": "{spacing.base.md}","$type": "dimension"},"large": {"$value": "{spacing.base.lg}","$type": "dimension"},"xlarge": {"$value": "{spacing.base.xl}","$type": "dimension"}},"layout": {"small": {"$value": "{spacing.base.md}","$type": "dimension"},"medium": {"$value": "{spacing.base.lg}","$type": "dimension"},"large": {"$value": "{spacing.base.xl}","$type": "dimension"}}}}
This structure allows you to swap out the base layer for each brand while maintaining the semantic relationships, creating a consistent naming convention regardless of which brand is being implemented.
Creating Brands with Style Dictionary Using Token Layers
With our token structure established, we can now configure Style Dictionary to compile brand-specific outputs while preserving the layer relationships. We'll generate both CSS custom properties and Sass variables to provide flexibility for different project requirements.
Configuring Style Dictionary
The following configuration compiles our tokens into CSS and Sass outputs for each theme:
import { globSync } from 'glob';import StyleDictionary from 'style-dictionary';// The brands we want to generateconst brands = ['raw', 'smackdown', 'nxt'];// The output formats we want to generateconst formats = ['css', 'scss'];// Function to get config for a specific brand and format (base tokens)function getStyleDictionaryConfig(brand, format) {// Base platform configuration (for both CSS and SCSS)const platformConfig = {[`${format}_base`]: {transformGroup: format,buildPath: `build/${format}/base/`,files: [{destination: `${brand}.${format}`,format: `${format}/variables`,filter: (token) => token.filePath.includes(`src/tokens/base/${brand}/`)}]}};// Removed the brand CSS files configuration herereturn {source: [`src/tokens/base/${brand}/**/*.tokens`,'src/tokens/semantic/**/*.tokens'],platforms: platformConfig};}// Only generate semantic once, not per brandfunction getSemanticConfig(format) {return {source: ['src/tokens/base/raw/**/*.tokens', // a brand (any brand) is needed to generate semantic tokens'src/tokens/semantic/**/*.tokens'],platforms: {[`${format}_semantic`]: {transformGroup: format,buildPath: `build/${format}/semantic/`,files: [{destination: `tokens.${format}`,format: `${format}/variables-semantic`,filter: (token) => token.filePath.includes('src/tokens/semantic/'),options: {outputReferences: true}}]}}};}// Helper function to safely get token valuefunction getTokenValue(token) {if (token && token.original && token.original.$value) {return token.original.$value;}if (token && token.value) {return token.value;}return token.value;}// Removed the brand wrapper format registration// Register custom format for CSS semantic variables with ds- prefixStyleDictionary.hooks.formats['css/variables-semantic'] = function({ dictionary, options }) {const semanticTokens = dictionary.allTokens.filter(token =>token.filePath.includes('src/tokens/semantic/'));const variables = semanticTokens.map((token) => {const name = token.path.join('-');const description = token.original.$description || '';const referenceValue = getTokenValue(token);if (typeof referenceValue === 'string' && referenceValue.startsWith('{')) {const refPath = referenceValue.replace(/^\{|\}$/g, '');const cssVarName = `var(--${refPath.replace(/\./g, '-')})`;return ` --ds-${name}: ${cssVarName};${description ? ` /* ${description} */` : ''}`;} else {return ` --ds-${name}: ${referenceValue};${description ? ` /* ${description} */` : ''}`;}}).join('\n');return `:root {\n${variables}\n}`;};// Register custom format for SCSS semantic variables with ds- prefixStyleDictionary.hooks.formats['scss/variables-semantic'] = function({ dictionary, options }) {const semanticTokens = dictionary.allTokens.filter(token =>token.filePath.includes('src/tokens/semantic/'));const variables = semanticTokens.map((token) => {const name = token.path.join('-');const description = token.original.$description || '';const referenceValue = getTokenValue(token);if (typeof referenceValue === 'string' && referenceValue.startsWith('{')) {const refPath = referenceValue.replace(/^\{|\}$/g, '');return `$ds-${name}: $${refPath.replace(/\./g, '-')};${description ? ` // ${description}` : ''}`;} else {return `$ds-${name}: ${referenceValue};${description ? ` // ${description}` : ''}`;}}).join('\n');return variables;};// Loop through each brand and format and build base tokensbrands.forEach(brand => {formats.forEach(format => {console.log(`Building ${format} tokens for ${brand}...`);const sd = new StyleDictionary(getStyleDictionaryConfig(brand, format));// For each platform in the configObject.keys(sd.config.platforms).forEach(platform => {sd.buildPlatform(platform);});});});// Build semantic tokens once for each formatformats.forEach(format => {console.log(`Building ${format} semantic tokens...`);const sd = new StyleDictionary(getSemanticConfig(format));// For each platform in the configObject.keys(sd.config.platforms).forEach(platform => {sd.buildPlatform(platform);});});console.log('Build completed!');
Explanation of Key Components
Creating the Style Dictionary Instance
This script first configures Style Dictionary to handle base tokens for multiple brands. Each brand has its own set of base tokens designated by its unique file paths in the src/tokens/base/
directory. The script systematically builds these tokens for each brand in the specified output formats (CSS and SCSS).
Key Points:
- Brands Configuration: The
brands
array specifies the brand names, which correspond to token file directories. - Formats: The
formats
array ensures outputs for both CSS and SCSS. - Configuration Function:
getStyleDictionaryConfig
dynamically generates the configuration for each brand and format combination. - Token Filtering: Filters help map input token files correctly to their corresponding brands.
Registering a Custom Format for Semantic Tokens
The script registers custom formats to generate variables with a ds-
namespace in both CSS and SCSS. This design ensures semantic tokens remain consistent across brands, leveraging shared properties.
Highlights:
- CSS Variable Registration: Custom functions format CSS with the
ds-
prefix, ensuring clarity and consistency. - Semantic Generation: Shared semantic tokens only need to be built once, fostering efficiency. Building All Platforms The script uses loops to iterate over each brand and format, systematically building and compiling tokens. It ensures separate brand builds to encapsulate unique identities while generating a single semantic output, unifying shared visual elements across the system. Finally, a console log indicates successful build completion, providing clear feedback on the process and confirming that all configurations have been executed correctly.
The Generated Outputs
After running the build script, Style Dictionary will generate the following files for each brand: CSS Custom Properties (build/css/raw/base.css)
:root {--color-base-primary: #d7182a;--color-base-secondary: #8b0000;--color-base-background: #ffcdd2;--color-base-text: #000000;--color-base-accent: #ffd700;--spacing-base-xs: 4px;--spacing-base-sm: 8px;--spacing-base-md: 16px;--spacing-base-lg: 24px;--spacing-base-xl: 32px;--font-family-primary: 'Roboto', sans-serif;--font-family-secondary: 'Open Sans', sans-serif;--font-weight-regular: 400;--font-weight-bold: 700;--font-size-small: 14px;--font-size-medium: 16px;--font-size-large: 24px;}
CSS Custom Properties (build/css/semantic/tokens.css)
:root {--ds-color-background-default: var(--color-base-background);--ds-color-text-default: var(--color-base-text);--ds-color-action-primary: var(--color-base-primary);--ds-color-action-secondary: var(--color-base-secondary);--ds-color-action-highlight: var(--color-base-accent);--ds-typography-heading-fontFamily: var(--font-family-primary);--ds-typography-heading-fontWeight: var(--font-weight-bold);--ds-typography-heading-fontSize: var(--font-size-large);--ds-typography-body-fontFamily: var(--font-family-secondary);--ds-typography-body-fontWeight: var(--font-weight-regular);--ds-typography-body-fontSize: var(--font-size-medium);--ds-typography-caption-fontFamily: var(--font-family-secondary);--ds-typography-caption-fontWeight: var(--font-weight-regular);--ds-typography-caption-fontSize: var(--font-size-small);--ds-spacing-element-xsmall: var(--spacing-base-xs);--ds-spacing-element-small: var(--spacing-base-sm);--ds-spacing-element-medium: var(--spacing-base-md);--ds-spacing-element-large: var(--spacing-base-lg);--ds-spacing-element-xlarge: var(--spacing-base-xl);--ds-spacing-layout-small: var(--spacing-base-md);--ds-spacing-layout-medium: var(--spacing-base-lg);--ds-spacing-layout-large: var(--spacing-base-xl);}
Sass Variables (build/scss/raw/base.scss)
$color-base-primary: #d7182a;$color-base-secondary: #8b0000;$color-base-background: #ffcdd2;$color-base-text: #000000;$color-base-accent: #ffd700;$spacing-base-xs: 4px;$spacing-base-sm: 8px;$spacing-base-md: 16px;$spacing-base-lg: 24px;$spacing-base-xl: 32px;$font-family-primary: 'Roboto', sans-serif;$font-family-secondary: 'Open Sans', sans-serif;$font-weight-regular: 400;$font-weight-bold: 700;$font-size-small: 14px;$font-size-medium: 16px;$font-size-large: 24px;
Sass Variables (build/scss/semantic/tokens.scss)
$ds-color-background-default: $color-base-background;$ds-color-text-default: $color-base-text;$ds-color-action-primary: $color-base-primary;$ds-color-action-secondary: $color-base-secondary;$ds-color-action-highlight: $color-base-accent;$ds-typography-heading-fontFamily: $font-family-primary;$ds-typography-heading-fontWeight: $font-weight-bold;$ds-typography-heading-fontSize: $font-size-large;$ds-typography-body-fontFamily: $font-family-secondary;$ds-typography-body-fontWeight: $font-weight-regular;$ds-typography-body-fontSize: $font-size-medium;$ds-typography-caption-fontFamily: $font-family-secondary;$ds-typography-caption-fontWeight: $font-weight-regular;$ds-typography-caption-fontSize: $font-size-small;$ds-spacing-element-xsmall: $spacing-base-xs;$ds-spacing-element-small: $spacing-base-sm;$ds-spacing-element-medium: $spacing-base-md;$ds-spacing-element-large: $spacing-base-lg;$ds-spacing-element-xlarge: $spacing-base-xl;$ds-spacing-layout-small: $spacing-base-md;$ds-spacing-layout-medium: $spacing-base-lg;$ds-spacing-layout-large: $spacing-base-xl;
Similar outputs will be generated for the othrt brands, each with their unique values but maintaining the same semantic structure.
You can find the code for this article on Github.
Using Style Dictionary for multiple brands with a token layer strategy unlocks a suite of advantages that transform how we can approach design systems.
Here's why this approach stands out:
- Seamless Consistency Across Platforms: With Style Dictionary, you can produce brand-specific tokens that cater to multiple platforms such as CSS, Sass, iOS, Android, and more. This ensures a uniform visual identity is maintained across all implementations.
- Centralised Brand Management: All brand definitions are housed within a single source of truth. This centralisation simplifies the update process and minimises inconsistencies, making maintenance a breeze.
- Scalability Built-In: As your product line expands, you can incorporate new brands without overhauling your entire system. Simply add new base token sets and watch your design system grow.
- Clear License of Separation: By distinguishing between raw values and their semantic meanings, both developers and designers can work more intuitively.
- Lowered Maintenance Load: By isolating modifications to the base layer, semantic tokens inherently mirror these updates. This can reduce overhead and maintains cohesion across your brands. By harnessing the power of Design Tokens and Style Dictionary and a layered token architecture, teams can establish resilient and maintainable multi-brand systems. As we continue to refine and evolve our design systems, using Design Tokens and integrating Style Dictionary presents a robust route toward flexible and scalable branding solutions. Stay tuned for our next installment, where we will dive into creating multiple themes for a single brand.