v3.0.0
Version 3.0 is now publicly released! We have been working hard the past few months on a number of features and improvements we wanted to combine into a major release. Though we intend that 3.0 to be backwards compatible, we thought it was a good idea to move to a new major version because we are changing a core part of how Style Dictionary works.
If you are starting a new project, you can install Style Dictionary and it will give you the latest version:
npm install --save-dev style-dictionary
If you have an existing project, you can upgrade to 3.0 by updating the version in your package.json
file to "style-dictionary": "^3.0.0"
and then run npm install
or you can use the latest
tag to update both your package.json
and package-lock.json
files:
npm install --save-dev style-dictionary@latest
If you find any bugs or issues, please file tickets so we can get things fixed. We have tested and reviewed 3.0 extensively, but there may be things we missed. Thanks!
Style Properties → Design Tokens
Style Dictionary is moving to the term "design tokens", both in documentation and in code. This has become the industry standard term for a while and it is time we respect that. Until now, Style Dictionary had called these "style properties" or just "properties", with some parts of the documentation also mentioning "design tokens". We want to be consistent with the direction of the community as well as in our documentation and code. We use the terms properties
and allProperties
in different APIs in Style Dictionary. To be consistent in documentation as well as code, we will be moving to using tokens
and allTokens
.
Don't worry! This change is backwards-compatible; you will still be able to use properties
and allProperties
wherever you currently do in your code. If you want, you can update those to tokens and allTokens and everything will work as expected. Moving forward, all examples and documentation will use the term "design tokens" and "tokens" rather than "style properties" and "properties". We do recommend using tokens
and allTokens
in new code from here on out!
Transitive transforms
This is the big one that required a big re-architecture of how the Style Dictionary build process works.
Up until now the build process looked like this: https://amzn.github.io/style-dictionary/#/build_process
After merging all of the token files, it would iterate through the merged object and transform tokens it found, but only do value transforms on tokens that did not reference another token. The original intent here was that a value of any reference should be the same for all references of it, so we only need to do a value transform once. Then after all tokens are transformed, resolve all aliases/references.
However, we heard from the community there were a number of reasons why someone might want to transform the value of an aliased token.
The new build process is similar, except that it recursively transforms and resolves aliases, only deferring a transform to the next cycle if the token has an unresolved alias. Each pass might reveal tokens that are ready to be transformed in the next pass. Take this example:
{
"color": {
"black": { "value": "#000000" },
"font": {
"primary": { "value": "{color.black.value}" },
"input": { "value": "{color.font.primary.value}" }
}
}
}
The first pass will transform the color.black
token because it doesn't have a reference. It will defer the transforms of color.font.primary
and color.font.input
because they do have references. Then it will resolve references to color.black.value
because it has been transformed.
The second pass will transform color.font.primary
because it no longer has a reference. It will then resolve the reference to color.font.primary.value
because it has been transformed.
The final pass will transform color.font.input
because it no longer has a reference. Now the build is complete because there are no more deferred transforms left.
Use cases this change opens up:
- Having variable references in outputs
- Combining values like using HSL for colors
- Modifying aliases like making a color lighter or darker
Example: https://github.com/amzn/style-dictionary/tree/main/examples/advanced/transitive-transforms
Thanks @mfal!
Output references
This is another big one. This has been one of the first issues we made back in 2017, issue 17! This adds support for outputting references in exported files. This is a bit hard to explain, so let's look at an example. Say you have this very basic set of design tokens:
// tokens.json
{
"color": {
"red": { "value": "#ff0000" },
"danger": { "value": "{color.red.value}" },
"error": { "value": "{color.danger.value}" }
}
}
With this configuration:
// config.json
{
"source": ["tokens.json"]
"platforms": {
"css": {
"transformGroup": "css",
"files": [{
"destination": "variables.css",
"format": "css/variables",
"options": {
// Look here 👇
"outputReferences": true
}
}]
}
}
}
This would be the output:
:root {
--color-red: #ff0000;
--color-danger: var(--color-red);
--color-error: var(--color-danger);
}
The css variables file now keeps the references you have in your Style Dictionary! This is useful for outputting themeable and dynamic code.
Without outputReferences: true
Style Dictionary would resolve all references and the output would be:
:root {
--color-red: #ff0000;
--color-danger: #ff0000;
--color-error: #ff0000;
}
Not all formats use the outputReferences
option because that file format might not support it (like JSON for example). The current list of formats that handle outputReferences
:
- css/variables
- scss/variables
- less/variables
- compose/object
- android/resources
- ios-swift/class.swift
- ios-swift/enum.swift
- flutter/class.dart
If you have custom formats you can make use of this feature too! The dictionary
object that is passed as an argument to the formatter function has 2 new methods on it: usesReference()
and getReference()
which you can use to get the reference name. Here is an example of that:
StyleDictionary.registerFormat({
name: `myCustomFormat`,
formatter: function(dictionary) {
return dictionary.allProperties.map(token => {
let value = JSON.stringify(token.value);
// the `dictionary` object now has `usesReference()` and
// `getReference()` methods. `usesReference()` will return true if
// the value has a reference in it. `getReference()` will return
// the reference to the whole token so that you can access its
// name or any other attributes.
if (dictionary.usesReference(token.original.value)) {
const reference = dictionary.getReference(token.original.value);
value = reference.name;
}
return `export const ${token.name} = ${value};`
}).join(`\n`)
}
})
Example: https://github.com/amzn/style-dictionary/tree/main/examples/advanced/variables-in-outputs
Custom parser support
We are pretty excited about this. Until now you could only define your design tokens in either JSON, JSON5, or plain Node modules. The addition of custom parser support allows you to define your tokens in any language you like! The most obvious use-case is to use YAML which has a cleaner and less verbose syntax than JSON. Now, the sky is the limit. Your source of truth can be in any file format you like as long as you can parse it into an object that Style Dictionary can understand. You register a custom parser the same way as you register a custom transform or format. A parser consists of a pattern to match against files, similar to the test attribute in a loader in Webpack, and a parse function which gets the file path and its contents and is expected to return an object.
- Example: https://github.com/amzn/style-dictionary/tree/main/examples/advanced/custom-parser
- YAML example: https://github.com/amzn/style-dictionary/tree/main/examples/advanced/yaml-tokens
Adding filePath and isSource entries on tokens
Style Dictionary adds some metadata on each token before any transforms take place in order to give more context for transforming and formatting tokens. For example, this design token:
{
"color": {
"red": { "value": "#ff0000" }
}
}
Turns into this:
{
"color": {
"red": {
"value": "#ff0000",
"name": "red", // adds a default 'name', which is the object key
"path": ["color","red"], // object path
"original": {
"value": "#ff0000"
} // copies the original object so you always have a clean copy
}
}
}
We are adding 2 new pieces of metadata to each token:
- filePath: A string representing the absolute path of the file that defines the token. This will help with debugging and if you want to output files based on the source files.
- isSource: A boolean representing if this file was defined as ‘source’ in the configuration as opposed to ‘include’ (or directly setting the ‘properties’ object). This can also help filtering tokens from output files you don't want to include.
Thanks @7studio!
Format helpers
Style Dictionary comes with built-in formats to generate basic files like CSS, Sass, and Less variables. We know that we can't build formats to fit everyone's exact needs, so you can write your own formats to output exactly what you want. However, one common ask was to use 90% of a built-in format, but tweak small parts of it. Before 3.0, the only way to do that would be to copy and paste the format source code from Style Dictionary into your own custom format and then make your tweaks.
In 3.0, we have added format helpers. Format helpers are functions that the built-in formats use for things like formatting each token definition line, sorting the tokens, and displaying a file header comment. These functions are now part of the public API, which you can access with StyleDictionary.formatHelpers. Format helpers should make it simple to build custom formats.
const StyleDictionary = require('style-dictionary');
const { formattedVariables } = StyleDictionary.formatHelpers;
module.exports = {
format: {
myFormat: ({ dictionary, options, file }) => {
const { outputReferences } = options;
return formattedVariables({
dictionary,
outputReferences,
formatting: {
prefix: '$',
separator: ':',
suffix: ';'
}
});
}
}
}
Example: https://github.com/amzn/style-dictionary/tree/main/examples/advanced/format-helpers
Updated format method arguments
We wanted to make the format method signature easier to work with by not relying on positional arguments and instead used named arguments in the form of an object that can be destructured. But we also didn't want to break anyone's custom formats so we did it in a backwards compatible way. We did this by overloading the first argument sent to the formatter method, which is the dictionary object.
Old way:
StyleDictionary.registerFormat({
name: 'myCustomFormat',
formatter: (dictionary, platform, file) {
// ...
}
});
New way:
StyleDictionary.registerFormat({
name: 'myCustomFormat',
formatter: ({dictionary, platform, file, options}) {
// ...
}
});
The old way will continue to work, but we recommend changing to the new way at some point. For a full list of the information sent to the formatter function, see: https://github.com/amzn/style-dictionary/blob/3.0/docs/formats.md#creating-formats
You might also notice a new part of the information sent to the formatter method: options
. This object will a merged version of an options
object at the platform-level configuration and file-level configuration where the file takes precedence. This allows for a cascading of configuration from the platform to the file and so you don't have to repeat the same options for multiple files like the showFileHeader
option.
{
"source": ["tokens/**/*.json"],
"platforms": {
"css": {
"options": {
"showFileHeader": false,
// ouputReferences is a new option added in 3.0!
"outputReferences": true
},
"transformGroup": "css",
"files": [{
// this file will inherit the platform options above
"destination": "variables.css",
"format": "css/variables"
},{
// this file overrides the platform options
"destination": "variablesWithoutReferences.css",
"format": "css/variables",
"options": {
"showFileHeader": true,
"outputReferences": false
}
}]
}
}
}
Previously, there wasn't much convention around adding extra configuration at the platform and file-level for transforms, formats, and actions. We want to start correcting that a bit by using an options
object at the file and platform level.
Custom file headers
When we started Style Dictionary, our intention was that the code it generates would not be checked into version control. To make that very apparent we added a code comment at the top of every built-in format that says it is a generated file with a timestamp of when it was generated. Over the years we have seen many different use-cases and patterns emerge from using Style Dictionary. One of those is actually checking in generated files to version control and using a pull request to review and verify the results. Having a time-stamped comment in generated files now becomes a bit of an obstacle. In the principle of extensibility and customization, you can now define your own file header comments for output files! This allows you to remove the timestamp if you want, write a custom message, use the version number of the package, or even use a hash of the source so that it only changes when the source changes.
You can use the custom file headers on any built-in format, or even use them in a custom format with the formatHelper.fileHeader function.
module.exports = {
//...
fileHeader: {
myFileHeader: (defaultMessage) => {
return [
...defaultMessage,
'hello, world!'
]
}
},
platforms: {
css: {
transformGroup: `css`,
files: [{
destination: `variables.css`,
format: `css/variables`,
fileHeader: `myFileHeader`
}]
}
}
}
Example: https://github.com/amzn/style-dictionary/tree/main/examples/advanced/custom-file-header
Typescript support
Style Dictionary has Typescript types now! We have added support both for generating typescript files with the typescript/es6-declarations and typescript/module-declarations formats, as well as type definitions for using the Style Dictionary module in a Typescript environment. The source code of Style Dictionary remains in plain Javascript for now, but many people have been wanting to use Style Dictionary in Typescript so as an interim solution we added just the type definitions. As an added bonus, the type definitions also help Javascript Intellisense in VSCode even if you aren't using Typescript in your project!
Thanks @AndrewLeedham!
More built-ins
We added support for: Jetpack Compose, React Native, and Stylus with built-in transforms, transform groups, and formats. Style Dictionary is built to be customized and extended, so any language or platform can be supported with customization. We do want to offer a core set of generic transforms and formats to get you started so that you don't have to write custom code for everything. If you think we are missing something, please let us know! Here are the formats, transforms, and transformGroups we added in 3.0:
Formats
javascript/module-flat
, thanks @noslouch!android/resources
stylus/variables
, thanks @klausbayrhammercompose/object
, thanks @bherbsttypescript/es6-declarations
, thanks @Tiamantitypescript/module-declarations
, thanks @Tiamanti
Transforms
size/pxToRem
, thanks @jbarreiros!size/object
, thanks @levi-piressize/compose/remToSP
, thanks @bherbstsize/compose/remToDP
, thanks @bherbstsize/compose/em
, thanks @bherbstcolor/composeColor
, thanks @bherbst- Made base pixel size configurable, thanks @jbarreiros!
Transform Groups
react-native
, thanks @levi-pirescompose
, thanks @bherbst
Bug fixes
- Trailing slash check on windows, thanks @JDansercoer!
- Clean config path in CLI, thanks @tehkaiyu!
- Fixing max call stack bug on json/nested format
- Fix transform options type, thanks @amalik2!
Other features
Better testing
To have higher confidence in the end-to-end experience of Style Dictionary we have added integration tests that run Style Dictionary as real users would. Integration tests are in integration and use snapshot testing to ensure the output of Style Dictionary from input, transforms, and formats remains the same and valid output.
We have also added more unit tests as well for some features we have added and bugs we have found. We added:
- 28 test suites
- 174 tests
- 69 snapshots
Dropping support for older versions of node
To be honest, we should have done this way sooner. Those versions of Node are no longer even in long-term support or maintenance. Hopefully this change should not affect anyone as you should all be on more recent versions of Node anyways.