— A Typescript implementation of the Design Tokens Format Module specification along with some utility functions
This repository aims to provide comprehensive tooling to work with the Design Tokens Format Module specification when it comes to parse, validate, resolve aliasing and (maybe) manage design token files.
⚠️ Please note, neither the DTCG specification nor this library are stable yet. The DTCG specification is currently under draft phase and the library might integrate unstable APIs for the sake of research.
According to the DTCG, Design Tokens are described using a JSON object made of arbitrary nested groups
, organizing tokens
.
While tokens
are responsible to carry the actual value ($value
), we also distinguish aliases
that help reference another token within the same JSON object using the {dot.path}
notation.
The DTCG does not give a name (yet?) to this very JSON object. For the sake of clarity, this library refers to it as
DesignTokenTree
.
At the top level definition we encounter the DesignTokenTree
. A simple recursive structure allowing deep group nesting where Design Tokens can be defined.
type DesignTokenTree = {
[name: string]: DesignToken | DesignTokenGroup | DesignTokenTree;
};
{
"colors": {
"primary": {
"$type": "color",
"$value": "#ff0000"
}
}
}
A DesignTokenGroup
is a simple object that can be used to group Design Tokens together. It is used to provide an arbitrary semantic meaning to a set of Design Tokens.
It can also define a $type
property, which will be inherited by all Design Tokens within the group.
export type DesignTokenGroup = {
$type?: DesignTokenType;
$description?: string;
};
{
"some": {
"nested": {
"groups": {}
}
}
}
A Design Token is recognized by the presence of a $value
property. It can be a simple primitive value, a complex object or an alias to another token of the same type.
The type must be specified most of the time, but the library will try to infer against JSONTypeName
from the value if not specified.
export type DesignToken = {
$value: DesignTokenValue;
$type?: DesignTokenType;
$description?: string;
$extensions?: JSONValue;
};
{
"border-default": {
"$type": "border",
"$value": {
"width": "1px",
"style": "solid",
"color": "#000000"
}
}
}
The DTCG specification defines a set of Design Token types that can be used to describe the value of a Design Token. The library provides a set of constants to use as $type
value.
As of today (Oct 2022), the following types are supported:
type DesignTokenType =
// JSON types
| 'string'
| 'number'
| 'boolean'
| 'null'
| 'object'
| 'array'
// DTCG types
| 'color'
| 'dimension'
| 'fontFamily'
| 'fontWeight'
| 'duration'
| 'cubicBezier'
// Composite types
| 'shadow'
| 'strokeStyle'
| 'border'
| 'transition'
| 'gradient'
| 'typography'
More details in the API section down below 👇
The DesignTokenAlias
is a special type of Design Token that references another token within the same Design Token Tree using the {dot.path}
notation.
type DesignTokenAlias = `{${string}}`;
{
"color": {
"base": {
"$type": "color",
"$value": "#ff0000"
},
"some-other-color": {
"$type": "color",
"$value": "{color.primary.base}"
}
}
}
To avoid having to specify the $type
Design Token of a given Group, the DTCG allows to define group-level $types
.
{
"colors": {
"$type": "color",
"primary": {
"$value": "#ff0000"
},
"secondary": {
"$value": "#00ff00"
}
}
}
colors.primary
and colors.secondary
will both be resolved with type color
.
$ npm install design-tokens-format-module
The first usage of the parser is to validate a Design Token Tree against the DTCG specification.
import { parseDesignTokens, DesignTokenTree } from "design-tokens-format-module";
const tokens: DesignTokenTree = {
"colors": {
"$type": "color",
"primary": {
"$value": "#ff0000"
}
}
};
const parsedTokens = parseDesignTokens(tokens);
This outputs almost the same structure:
// parsedTokens =
{
"colors": {
"$type": "color",
"primary": {
"$type": "color", // <-- Resolved from the group-level $type
"$value": "#ff0000"
}
}
}
But where tokens
was a DesignTokenTree
, parsedTokens
is now a ConcreteDesignTokenTree
.
The library provides a ConcreteDesignTokenTypeValueGuard
type offering type safety in consumer code. This is a special type of DesignTokenTree
that has been validated against the DTCG specification and defines only valid $type<>$value
combinations.
In order to fully validate the tree and make it ready for further serialization operations, the parser takes an optional resolveAliases
parameter that will resolve all aliases in the tree.
import { parseDesignTokens, DesignTokenTree } from "design-tokens-format-module";
const tokens: DesignTokenTree = {
colors: {
$type: 'color',
primary: {
$value: '#000000',
$description: 'This is a primary color',
},
secondary: {
$value: '{colors.primary}',
},
},
};
const parsedTokens = parseDesignTokens(tokens, { resolveAliases: true });
parsedTokens.colors.secondary
now holds the content of its alias.
// parsedTokens =
{
"colors": {
"$type": "color",
"primary": {
"$type": "color",
"$value": "#000000",
"$description": "This is a primary color"
},
"secondary": {
"$type": "color",
"$value": {
"$type": "color",
"$value": "#000000",
"$description": "This is a primary color"
}
}
}
}
Along the development of this library, some metadata information, such as the path to a design token or the kind of a tree node.
the parser takes an optional publishMetadata
parameter that will add metadata to the concrete tree.
Additions are:
_kind
property on each node ('group' | 'token' | 'alias'
)_path
property on each node (Array<string>
)_name
property onConcreteDesignTokenAlias
only to reference the original name of the alias
import { parseDesignTokens, DesignTokenTree } from "design-tokens-format-module";
const input: DesignTokenTree = {
'a-color': {
$type: 'color',
$value: '#000000',
$description: 'This is a color',
},
};
const parsedTokens = parseDesignTokens(input, { publishMetadata: true });
parsedTokens
has now _
prefixed properties.
// parsedTokens =
{
"a-color": {
"$type": "color",
"$value": "#000000",
"$description": "This is a color",
"_kind": "token",
"_path": ["a-color"]
}
}
import { parseDesignTokens, DesignTokenTree } from "design-tokens-format-module";
const tokens: DesignTokenTree = {
colors: {
$type: 'color',
primary: {
$value: '#000000',
$description: 'This is a primary color',
},
secondary: {
$value: '{colors.primary}',
},
},
};
const parsedTokens = parseDesignTokens(tokens, { resolveAliases: true, publishMetadata: true });
// parsedTokens =
{
"colors": {
"$type": "color",
"_kind": "group",
"_path": ["colors"],
"primary": {
"$type": "color",
"$value": "#000000",
"$description": "This is a primary color",
"_kind": "token",
"_path": ["colors", "primary"]
},
"secondary": {
"$type": "color",
"$value": {
"$type": "color",
"$value": "#000000",
"$description": "This is a primary color",
"_kind": "alias",
"_path": ["colors", "primary"],
"_name": "primary" // <-- The original name of the alias
},
"_kind": "token",
"_path": ["colors", "secondary"]
}
}
}
The DesignTokenTree
is the JSON Object data structure that represents the entire design tokens document.
type DesignTokenTree = {
[name: string]: DesignToken | DesignTokenGroup | DesignTokenTree;
};
At each level of the token tree, we can have either an actual design token, a token group or yet another token tree, recursively.
In order to avoid duplication of declarations, a DesignToken
can reference another DesignToken
using the $value
property and the {token.path}
syntax.
type DesignTokenAlias = `{${string}}` // e.g. {colors.primary}
We distinguish 3 categories of design tokens:
- JSON values (string, number, boolean, ...)
- Primitive Design Tokens (color, duration, ...)
- Composite Design Tokens (border, shadow, ...)
They are the only types we might infer if not provided explicitly in the given TokenTree
.
type JSONTokenType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'
type name: 'color'
type ColorValue = `#${string}` | DesignTokenAlias
type name: 'dimension'
type DimensionValue = `${number}px` | `${number}rem` | DesignTokenAlias; // 1px | 1rem
type name: 'fontFamily'
type FontFamilyValue = string | string[] | DesignTokenAlias; // "Helvetica" | ["Helvetica", "Arial", sans-serif]
type name: 'fontWeight'
type FontWeightValue =
| number // [1-1000]
| FontWeightNomenclature[keyof FontWeightNomenclature]['value'] // 'thin' | 'hairline' | 'extra-light' | 'ultra-light' | 'light' | 'normal' | 'regular' | 'book' | 'medium' | 'semi-bold' | 'demi-bold' | 'bold' | 'extra-bold' | 'ultra-bold' | 'black' | 'heavy' | 'extra-black' | 'ultra-black'
| DesignTokenAlias;
type name: 'duration'
type DurationValue = `${number}ms` | DesignTokenAlias; // 100ms
type name: 'cubicBezier'
type CubicBezierValue =
| [P1x: number, P1y: number, P2x: number, P2y: number]
| DesignTokenAlias;
type name: 'shadow'
type ShadowValue =
| {
color: ColorValue;
offsetX: DimensionValue;
offsetY: DimensionValue;
blur: DimensionValue;
spread: DimensionValue;
}
| DesignTokenAlias;
type name: 'strokeStyle'
type StrokeStyleValue =
| 'solid'
| 'dashed'
| 'dotted'
| 'double'
| 'groove'
| 'ridge'
| 'outset'
| 'inset'
| {
dashArray: DimensionValue[];
lineCap: 'round' | 'butt' | 'square';
}
| DesignTokenAlias;
type name: 'border'
type BorderValue =
| {
color: ColorValue;
width: DimensionValue;
style: StrokeStyleValue;
}
| DesignTokenAlias;
type name: 'transition'
type TransitionValue =
| {
duration: DurationValue;
delay: DurationValue;
timingFunction: CubicBezierValue;
}
| DesignTokenAlias;
type name: 'gradient'
type GradientValue =
| Array<{
color: ColorValue;
position: JSONNumberValue;
}>
| DesignTokenAlias;
type name: 'typography'
type TypographyValue =
| {
fontFamily: FontFamilyValue;
fontSize: DimensionValue;
fontWeight: FontWeightValue;
letterSpacing: DimensionValue;
lineHeight: JSONStringValue;
}
| DesignTokenAlias;
declare function parseDesignTokens<A extends boolean, M extends boolean>(
input: TokenTree,
parserOptions: {
publishMetadata?: M; // defaults to false
resolveAliases?: A; // defaults to false
}
): ConcreteTokenTree<A, M>;
The function handles :
- Validation of the input against the Design Token spec
- Resolution of the
$type
field for any token - Optional resolution of aliases
- Optional population of
_
prefixed metadata
⚠️ Limitation: The detection of circular aliasing does not work withoutparserOptions.resolveAliases
set totrue
.
While constructing a Design Token Tree, you may need to validate token values individually. The library provides a validateDesignTokenValue
based on the zod validation library.
declare function validateDesignTokenValue(
tokenType: DesignTokenType,
tokenValue: DesignTokenValue
): any
Note: return type must be improved
Since token names and group names are meant to be used in {dot.path}
notation, we need to ensure they avoid the 3 characters: .
, {
, }
.
declare function validateDesignTokenAndGroupName(name: string): string
Used to deduce if a given string is an alias following the {dot.path}
notation.
declare function matchIsDesignTokenAlias(value: any): boolean
type JSONTypeName = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'
- Implement the latest specification Typescript type definition
- Add a parse function to traverse any given token tree
- Resolve aliases
- Add validation logic for all known token types
- Expose validation only API
- Expose parse API with options for alias resolution and metadata population
- Expose proper types for external consumers
- Add support for circular aliasing detection when not resolving aliases