Looking for a Front-End Developer and Design Systems Practitioner?

I currently have some availabilty. Let's talk!.

Always Twisted

A Design Tokens Workflow (part 9)

Implementing Multi-Brand Theming with Style Dictionary

  1. Getting Started With Style Dictionary
  2. Outputting to Different Formats with Style Dictionary
  3. Beyond JSON: Exploring File Formats for Design Tokens
  4. Converting Tokens with Style Dictionary
  5. Organising Outputs with Style Dictionary
  6. Layers, referencing tokens in Style Dictionary
  7. Implementing Light and Dark Mode with Style Dictionary
  8. Implementing Light and Dark Mode with Style Dictionary (part 2)
  9. Implementing Multi-Brand Theming with Style Dictionary – You are here
  10. 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:

Code languagejson
{
"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:

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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

Code languagejson
{
"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:

Code languagejavascript
import { globSync } from 'glob';
import StyleDictionary from 'style-dictionary';
// The brands we want to generate
const brands = ['raw', 'smackdown', 'nxt'];
// The output formats we want to generate
const 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 here
return {
source: [
`src/tokens/base/${brand}/**/*.tokens`,
'src/tokens/semantic/**/*.tokens'
],
platforms: platformConfig
};
}
// Only generate semantic once, not per brand
function 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 value
function 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- prefix
StyleDictionary.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- prefix
StyleDictionary.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 tokens
brands.forEach(brand => {
formats.forEach(format => {
console.log(`Building ${format} tokens for ${brand}...`);
const sd = new StyleDictionary(getStyleDictionaryConfig(brand, format));
// For each platform in the config
Object.keys(sd.config.platforms).forEach(platform => {
sd.buildPlatform(platform);
});
});
});
// Build semantic tokens once for each format
formats.forEach(format => {
console.log(`Building ${format} semantic tokens...`);
const sd = new StyleDictionary(getSemanticConfig(format));
// For each platform in the config
Object.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:

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:

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)

Code languagecss
: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)

Code languagecss
: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)

Code languagescss
$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)

Code languagescss
$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:

  1. 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.
  2. 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.
  3. 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.
  4. Clear License of Separation: By distinguishing between raw values and their semantic meanings, both developers and designers can work more intuitively.
  5. 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.

Wondering how to output tokens into different formats for different platforms?

I can configure tools to output tokens into the formats your teams need.

get in touch!