Setting up a Next.js project
As a starting point for building a test project, we used a Next.js Typescript template. This boilerplate implements core principles of JAMstack and allows to quickly create serverless applications.
To set up the project environment, it is necessary to install the following dependencies:
// package.json
dependencies: {
"style-dictionary": "",
"token-transformer": "",
"tailwindcss-box-shadow": "", // opt
"tinycolor2": "", // opt
}
Additionally, don't forget to regularly update your dependencies to ensure that token modifications are processed correctly.
The project has a standard file structure. The design tokens are placed within the styles directory. Additionally, the build.js file, which runs Style Dictionary, is located in the scripts folder.
├── styles
│ ├── scripts
│ │ ├── build.js
│ │ └── fns.js
│ ├── tokens
│ │ ├── input
│ │ │ ├── base
│ │ │ │ └── global.json
│ │ │ └── themes
│ │ │ ├── dark
│ │ │ │ └── dark.json
│ │ │ └── light
│ │ │ └── light.json
│ │ └── output
│ │ ├── dark.json
│ │ ├── global.json
│ │ └── light.json
│ ├── dark.css
│ ├── global.css
│ ├── index.css
│ └── light.css
Automating Design Token updates
The Figma project is synchronized with the repository, and when the designer pushes any changes, that triggers a pipeline, which transforms raw data into CSS variables.
Design Token files must go through several transformation steps to return valid style sheets. The token-transformer utility replaces the references with calculated values so that the JSON object conforms to the Style Dictionary standards. The --expandTypography option can be used to convert every font-related property into an individual object.
// package.json
"scripts": {
"transform-tokens-dark": "npx token-transformer app/ui/styles/tokens/input/themes/dark app/ui/styles/tokens/output/dark.json",
"transform-tokens-global": "npx token-transformer app/ui/styles/tokens/input/base app/ui/styles/tokens/output/global.json --expandTypography",
"transform-tokens-light": "npx token-transformer app/ui/styles/tokens/input/themes/light app/ui/styles/tokens/output/light.json",
"transform-tokens": "yarn transform-tokens-light && yarn transform-tokens-dark && yarn transform-tokens-global",
"tokens": "node app/ui/styles/scripts/build.js"
}
Commands in package.json support design token workflow:
// Transform design tokens
yarn transform-tokens
// Build and update CSS variables
yarn tokens
Global style settings
In global.json file, we've established a foundational set of design variables that are essential across the entire project. This includes crucial aspects like typography, size and z-index.
Style Dictionary allows us to define functions and then to modify input values. For example, you can specify the letter-spacing property in ems.
// build.js
function transformLetterSpacing(value) {
if (value.endsWith('%')) {
const percentValue = value.slice(0, -1);
return `${percentValue / 100}em`;
}
return value;
}
StyleDictionaryPackage.registerTransform({
name: 'size/letterspacing',
type: 'value',
transitive: true,
matcher: (token) => token.type === 'letterSpacing',
transformer: (token) =>
transformLetterSpacing(token.value)
});
Theme-specific styles. Dark and Light mode definitions
In dark.json and light.json files, we focus on theme-specific styles, primarily defining colors and shadows tailored for dark and light modes.
The transform function can combine the number of parameters that define the appearance of a box-shadow into a CSS variable. The resulting value can be used in the theme configuration file after installing the tailwindcss-box-shadow plugin.
// tailwind.config.js
module.exports = {
content: ['./app/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class',
theme: {},
plugins: [require('tailwindcss-box-shadow')]
}
// build.js
StyleDictionaryPackage.registerTransform({
name: "shadow/css",
type: "value",
transitive: true,
matcher: (token) => token.type === "boxShadow",
transformer: (token) => {
const shadow = Array.isArray(token.value) ? token.value : [token.value];
const value = shadow.map((s) => {
const { x, y, blur, spread, color } = s;
return `${x}px ${y}px ${blur}px ${spread}px ${tinycolor(color).toHslString()}`;
});
return value.join(", ");
},
});
Color Modifiers
Tokens Studio uses modifiers for fine-tuning color tokens, including lightening, darkening, mixing, and adjusting opacity. By creating transformers like color/hslAdjust in Style Dictionary, we can adapt tokens, darkening colors by a specified percentage and returning the result in HSL format. This approach allows for dynamic visual changes in interface elements, for example, darkening the hover token by 27% when the user hovers over it.
// light.json
"hover": {
"value": "{color.blue.500}",
"type": "color",
"$extensions": {
"studio.tokens": {
"modify": {
"type": "darken",
"value": "0.27",
"space": "hsl"
}
}
}
}
// build.js
function resolveTokenValue(token, dictionary) {
if (
typeof token.value === 'string' &&
token.value.startsWith('{') &&
token.value.endsWith('}')
) {
const resolvedToken = dictionary.getReferences(token.value);
return resolvedToken ? resolvedToken.value : token.value;
}
return token.value;
}
function transformHSL(token, dictionary) {
const resolvedValue = resolveTokenValue(token, dictionary);
let color = tinycolor(resolvedValue);
const modification = token.$extensions?.['studio.tokens']?.modify;
if (modification && modification.type === 'darken') {
color = color.darken(parseFloat(modification.value) * 100);
}
return color.toHslString();
}
StyleDictionaryPackage.registerTransform({
name: 'color/hslDarken',
type: 'value',
matcher: (token) => token.type === 'color' && token.$extensions,
transformer: (token, dictionary) => transformHSL(token, dictionary),
});
Theme
Finally, during the building process, a set of CSS variables will be created. Style Dictionary will add selectors to style sheets as a combination of a :root pseudo-class and a themed class. These can be used later to swap from light to dark mode.
// build.js
files: [
{
destination: `${theme}.css`,
format: 'css/variables',
selector: `:root.${theme}`
}
]
The Tailwind configuration file stores the object that represents the current theme. And Design Tokens can be attached to component styles through CSS variables. Thus, we can update the external presentation of the User Interface by changing the values of the tailwind.config.js.
// tailwind.config.js
theme: {
colors: {
primary: {
DEFAULT: 'var(--color-primary-default)'
}
}
}
To maintain consistency in UI design, we created a ThemeProvider component and placed it at the top-level of the React tree. That wrapper uses the Context API to pass the current theme data down to child components.
// App.tsx
export const App = () => (
<ThemeContextWrapper>
{/* children */}
</ThemeContextWrapper>
);
Top comments (0)