A Design Tokens Workflow (part 8)
Implementing Light and Dark Mode with Style Dictionary (part 2)
- 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) – You are here
On This Page:
In the last article in this series we looked at how we could convert our design tokens into CSS for light and dark modes using Style Dictionary. We created some new formats to generate CSS that used prefers-color-scheme
and also generate separate CSS files for both light and dark mode or a single CSS file that would incorporate both.
In this article, we will explore two additional methods for implementing light and dark modes in your design system using Style Dictionary. Building on our previous discussion, we will focus on how to add a data attribute or CSS class for dark mode, as well as generating relevant tokens using the CSS light-dark()
function. These approaches enhance the flexibility and adaptability of your design system, allowing for a smoother user experience.
Adding a Data Attribute or CSS Class for Dark Mode
To provide users with a seamless experience when toggling between light and dark modes, we can utilize a data attribute or CSS class. This method allows developers to apply dark mode styles conditionally based on the presence of a specific attribute or class in the HTML. Additionally, developers can implement a JavaScript function to toggle these modes dynamically, enabling users to switch modes with a button click.
Creating Some Design Tokens
Again, we will have some base layer tokens to choose from at the semantic layer to provide meaningful, purposeful names for the design decisions for light and dark mode.
{"color": {"base": {"white": {"$value": "#FFFFFF","$type": "color"},"black": {"$value": "#000000","$type": "color"},"gray": {"100": {"$value": "#F7F7F7","$type": "color"},"200": {"$value": "#E1E1E1","$type": "color"},"300": {"$value": "#CFCFCF","$type": "color"},"400": {"$value": "#B3B3B3","$type": "color"},"500": {"$value": "#A0A0A0","$type": "color"},"600": {"$value": "#7D7D7D","$type": "color"},"700": {"$value": "#5A5A5A","$type": "color"},"800": {"$value": "#3D3D3D","$type": "color"},"900": {"$value": "#1A1A1A","$type": "color"}},"primary": {"$value": "#007BFF","$type": "color"},"secondary": {"$value": "#6C757D","$type": "color"}}}}
We will then setup our semantic tokens like we did for single token file in the last article in this series.
{"color": {"background": {"$value": "{color.base.white}","$mods": {"dark": "{color.base.black}"},"$type": "color","$description": "Background color for light mode."},"text": {"$value": "{color.base.black}","$mods": {"dark": "{color.base.white}"},"$type": "color","$description": "Text color for light mode."},"card": {"background": {"$value": "{color.base.gray.200}","$mods": {"dark": "{color.base.gray.800}"},"$type": "color","$description": "Background color for cards in light mode."},"border": {"$value": "{color.base.gray.600}","$mods": {"dark": "{color.base.gray.400}"},"$type": "color","$description": "Border color for cards in light mode."}}},"button": {"background": {"primary": {"$value": "{color.base.primary}","$type": "color","$description": "Primary button background color in dark mode."},"secondary": {"$value": "{color.base.secondary}","$type": "color","$description": "Secondary button background color in dark mode."}},"text": {"$value": "{color.base.white}","$type": "color","$description": "Text color for buttons in dark mode."}}}
Setting Up the Build Process
We now need to set up the script that sets up the build process for generating CSS variables that accommodate both light and dark modes, including the necessary data attribute and CSS class.
import { globSync } from 'glob';import StyleDictionary from 'style-dictionary';const tokenFiles = globSync('src/tokens/**/*.tokens');const HEADER_COMMENT = `/*** Do not edit directly, this file was auto-generated.*/\n\n`;const myStyleDictionary = new StyleDictionary({source: tokenFiles,platforms: {css_base: {transformGroup: 'css',buildPath: 'build/css/base/',files: [{destination: 'colors.css',format: 'css/variables',filter: (token) => token.filePath.includes('base'),},],},css_themed: {transformGroup: 'css',buildPath: 'build/css/combined/',files: [{destination: 'colors.css',format: 'css/variables-themed',},],},},});StyleDictionary.hooks.formats['css/variables-themed'] = function({ dictionary }) {const semanticTokens = dictionary.allTokens.filter(token =>token.filePath.includes('combined'));const variables = semanticTokens.map((token) => {const { name } = token;const description = token.original.$description;const baseVariableName = token.original['$value'].replace(/^\{|\}$/g, '');const lightCssVariableName = `var(--${baseVariableName.replace(/\./g, '-').replace(/_/g, '-')})`;const darkModeValue = token.original.$mods?.dark;const darkCssVariableName = darkModeValue ? `var(--${darkModeValue.replace(/^\{|\}$/g, '').replace(/\./g, '-').replace(/_/g, '-')})` : null;return {light: ` --${name}: ${lightCssVariableName};${description ? ` /* ${description} */` : ''}`,dark: darkCssVariableName ? ` --${name}: ${darkCssVariableName};${description ? ` /* ${description} */` : ''}` : null,};});const lightVariables = variables.map(v => v.light).join('\n');const darkVariables = variables.map(v => v.dark).filter(Boolean).join('\n');return `${HEADER_COMMENT}:root {\n${lightVariables}\n}\n\n@media (prefers-color-scheme: dark) {\n :root{\n${darkVariables}\n }\n}\n\n[data-mode='dark'], .mode-dark {\n${darkVariables}\n}`;};myStyleDictionary.buildAllPlatforms();console.log('Build completed!');
Explanation of the Script
- Data Attribute and CSS Class: The generated CSS will include styles that apply when a [data-mode='dark'] attribute or a .mode-dark class is present. This provides flexibility in how dark mode styles are applied, allowing developers to choose the method that best fits their project.
// Data Attribute and CSS Classreturn `${HEADER_COMMENT}:root {\n${lightVariables}\n}\n\n@media (prefers-color-scheme: dark) {\n :root{\n${darkVariables}\n }\n}\n\n[data-mode='dark'], .mode-dark {\n${darkVariables}\n}`;
- CSS Variable Generation: The script extracts semantic tokens and generates CSS variables for both light and dark modes. The dark mode variables are included within a media query for automatic application based on user preferences, while also being available for manual toggling via the data attribute or class.
// CSS Variable Generationconst variables = semanticTokens.map((token) => {const { name } = token;const description = token.original.$description;const baseVariableName = token.original['$value'].replace(/^\{|\}$/g, '');const lightCssVariableName = `var(--${baseVariableName.replace(/\./g, '-').replace(/_/g, '-')})`;const darkModeValue = token.original.$mods?.dark;const darkCssVariableName = darkModeValue ? `var(--${darkModeValue.replace(/^\{|\}$/g, '').replace(/\./g, '-').replace(/_/g, '-')})` : null;return {light: ` --${name}: ${lightCssVariableName};${description ? ` /* ${description} */` : ''}`,dark: darkCssVariableName ? ` --${name}: ${darkCssVariableName};${description ? ` /* ${description} */` : ''}` : null,};});
The Generated CSS
After running this build script, Style Dictionary will generate one CSS file for the colours in the semantic tokens layer. We now have CSS declarations that are nested within a data-attribute and CSS class. This script also provides both prefers-color-scheme CSS declarations and styles based on data attributes or CSS classes, ensuring a comprehensive approach to theming. While the media query automatically detects user preferences for light or dark mode, the data attribute and CSS class allow users to manually toggle modes (using JavaScript).
/*** Do not edit directly, this file was auto-generated.*/:root {--color-background: var(--color-base-white);--color-text: var(--color-base-black);--color-card-background: var(--color-base-gray-200);--color-card-border: var(--color-base-gray-600);--button-background-primary: var(--color-base-primary);--button-background-secondary: var(--color-base-secondary);--button-text: var(--color-base-white);}@media (prefers-color-scheme: dark) {:root {--color-background: var(--color-base-black);--color-text: var(--color-base-white);--color-card-background: var(--color-base-gray-800);--color-card-border: var(--color-base-gray-400);}}[data-mode='dark'], .mode-dark {--color-background: var(--color-base-black);--color-text: var(--color-base-white);--color-card-background: var(--color-base-gray-800);--color-card-border: var(--color-base-gray-400);}
Generating the Relevant Tokens in the CSS light-dark() Function
Another effective method for managing light and dark modes is to utilize a light-dark() function in CSS. This function simplifies the process of switching between light and dark mode styles, allowing developers to specify how styles should adapt based on the user's preference
Sara Joy has an excellent article on CSS-Tricks explaining the wonders of the light-dark() CSS function
Setting Up the Build Process
Here’s how to implement the light-dark() function in your CSS:
import { globSync } from 'glob';import StyleDictionary from 'style-dictionary';const tokenFiles = globSync('src/tokens/**/*.tokens');const HEADER_COMMENT = `/*** Do not edit directly, this file was auto-generated.*/\n\n`;const myStyleDictionary = new StyleDictionary({source: tokenFiles,platforms: {css_base: {transformGroup: 'css',buildPath: 'build/css/base/',files: [{destination: 'colors.css',format: 'css/variables',filter: (token) => token.filePath.includes('base'),},],},css_themed: {transformGroup: 'css',buildPath: 'build/css/combined/',files: [{destination: 'colors.css',format: 'css/variables-themed',},],},},});StyleDictionary.hooks.formats['css/variables-themed'] = function({ dictionary }) {const semanticTokens = dictionary.allTokens.filter(token =>token.filePath.includes('combined'));const variables = semanticTokens.map((token) => {const { name } = token;const description = token.original.$description;const baseVariableName = token.original['$value'].replace(/^\{|\}$/g, '');const lightCssVariableName = `var(--${baseVariableName.replace(/\./g, '-').replace(/_/g, '-')})`;const darkModeValue = token.original.$mods?.dark;const darkCssVariableName = darkModeValue ? `var(--${darkModeValue.replace(/^\{|\}$/g, '').replace(/\./g, '-').replace(/_/g, '-')})` : null;if (darkCssVariableName) {return ` --${name}: light-dark(${lightCssVariableName}, ${darkCssVariableName});`;} else {return ` --${name}: ${lightCssVariableName};`;}}).join('\n');return `${HEADER_COMMENT}:root {\n${variables}\n}`;};myStyleDictionary.buildAllPlatforms();console.log('Build completed!');
Explanation of the Script
- light-dark() Function: The light-dark() function takes two parameters: the light mode variable and the dark mode variable. This allows for a concise way to define styles that adapt based on the user's preference, making it easier to manage changes in themes without duplicating styles.
if (darkCssVariableName) {return ` --${name}: light-dark(${lightCssVariableName}, ${darkCssVariableName});`;} else {return ` --${name}: ${lightCssVariableName};`;}
- CSS Variable Generation: Similar to the previous script, this one extracts semantic tokens and generates CSS variables. The key difference is the use of the light-dark() function, which simplifies the CSS output and enhances readability.
const variables = semanticTokens.map((token) => {const { name } = token;const description = token.original.$description;const baseVariableName = token.original['$value'].replace(/^\{|\}$/g, '');const lightCssVariableName = `var(--${baseVariableName.replace(/\./g, '-').replace(/_/g, '-')})`;const darkModeValue = token.original.$mods?.dark;const darkCssVariableName = darkModeValue ? `var(--${darkModeValue.replace(/^\{|\}$/g, '').replace(/\./g, '-').replace(/_/g, '-')})` : null;// Use light-dark() function for dark mode variablesif (darkCssVariableName) {return ` --${name}: light-dark(${lightCssVariableName}, ${darkCssVariableName});`;} else {return ` --${name}: ${lightCssVariableName};`;}}).join('\n');
The Generated CSS
After running this script we get a single CSS file for the colours that were in the semantic layer of the design tokens.
/*** Do not edit directly, this file was auto-generated.*/:root {--color-background: light-dark(var(--color-base-white), var(--color-base-black));--color-text: light-dark(var(--color-base-black), var(--color-base-white));--color-card-background: light-dark(var(--color-base-gray-200), var(--color-base-gray-800));--color-card-border: light-dark(var(--color-base-gray-600), var(--color-base-gray-400));--button-background-primary: var(--color-base-primary);--button-background-secondary: var(--color-base-secondary);--button-text: var(--color-base-white);}
Wrapping Up
In [the first article, we discussed various methods for implementing light and dark modes using design tokens with Style Dictionary. Building on that foundation, here are five effective ways to generate CSS for light and dark modes:
- Multiple Files for Light and Dark Modes (Two CSS Files): Create separate token files for light and dark modes, generating distinct CSS files for each mode. This ensures clear separation and easy management of styles, allowing for straightforward updates and maintenance. View on GitHub
- Multiple Files for Light and Dark Modes (One CSS File): Generate a single CSS file that consolidates styles for both light and dark modes. This method utilizes the prefers-color-scheme media feature to apply the appropriate styles based on user preferences, providing a seamless experience. View on GitHub
- Single Token File Incorporating Light and Dark Modes (One CSS File): Author a single token file that contains both light and dark mode styles. This approach allows for a cohesive view of design tokens and facilitates easier management and updates, generating a single CSS file that includes all necessary styles. View on GitHub
- Data Attribute or CSS Class for Dark Mode: Implement a data attribute or CSS class to enable manual toggling between light and dark modes. This method allows developers to conditionally apply dark mode styles based on the presence of a specific attribute or class in the HTML, enhancing user customization. View on GitHub
- Using the CSS light-dark() Function: Leverage the CSS light-dark() function to define styles that adapt based on user preferences. This function simplifies the process of switching between light and dark mode styles, allowing developers to specify how styles should adjust without duplicating code.View on GitHub
These methods provide flexibility and adaptability in your design system, ensuring that users have a smooth and personalized experience when interacting with your application.