17 min read
In the first article of the Ultimate Guide to Design Tokens, we saw that a design token is a design decision written as data, in JSON, so it can be platform-independent. But “JSON” alone is not enough for two tools to understand each other. Each one could invent its own structure, its own property names and its own way of writing a color.
That is what the DTCG, the Design Tokens Community Group, exists to solve. In this article, you will learn what this specification is, how a token file is structured and, above all, which token types exist and how each one is written, from the simplest (color, dimension) to the composite ones (shadow, typography, border).
What is the DTCG?
The DTCG (Design Tokens Community Group) is the W3C community group that maintains the specification of the file format for exchanging design tokens between tools.
The group brings together people from design system teams, tool vendors and independent specialists.
On October 28, 2025, the group published the first stable version, called 2025.10. It is split into modules:
- Format Module: file structure, groups, aliases, simple and composite types.
- Color Module: how to represent colors in different color spaces.
- Resolver Module: how to combine sets of tokens to produce variations such as themes and brands (the subject of the article on theming).
One important detail: the specification is a Community Group report. It is not an official W3C standard and is not on the W3C standards track. The document itself describes it as stable and intended for implementation. Margin note: stable, but not a W3C standard
In practice, the official site designtokens.org lists tools such as Figma, Style Dictionary, Tokens Studio, Penpot, Sketch and zeroheight among the group’s adopters and participants.
The goal is simple: a token file written today should be readable by design tools, translation tools (like Style Dictionary) and documentation tools, without custom-built glue code.
How a token file is organized
A token file is a JSON document in which objects with a $value are tokens and objects without a $value are groups, which organize tokens and compose their names.
File format
Token files are JSON. The specification recommends:
- Extensions:
.tokensor.tokens.json. - MIME type:
application/design-tokens+json(application/jsonis also accepted).
Token, name and value
An object that has a $value property is a token. The key of the parent object is the token name. Both name and value are required.
{
"color-brand": {
"$type": "color",
"$value": {
"colorSpace": "srgb",
"components": [0, 0.4, 0.8]
}
}
}
Every property defined by the specification starts with $. For that reason, token and group names cannot start with $. In addition, the characters {, } and . are forbidden in any name, because they are part of the alias and path syntax.
Names are case-sensitive. Even so, avoid tokens that differ only by case (font-size and FONT-SIZE): when exported to other languages, they can generate duplicate variables.
Optional properties
Besides $value, a token can have:
| Property | What it is for |
|---|---|
$type | Declares the type of the value (color, dimension, shadow…) |
$description | Plain text explaining the token’s purpose |
$extensions | Tool- or team-specific data, with a key in reverse domain name notation |
$deprecated | true, false or a string explaining the deprecation and the replacement |
Tools must preserve the $extensions they do not understand. That way, a file can pass through several tools without losing metadata.
Groups
A group is an object without a $value. It organizes tokens and composes names. The specification is explicit: groups are arbitrary, and tools should not infer meaning or type from a group’s name.
{
"color": {
"$type": "color",
"$description": "System colors",
"brand": {
"$value": {
"colorSpace": "srgb",
"components": [0, 0.4, 0.8]
}
}
}
}
Groups also accept $type, $description, $deprecated, $extensions and $extends.
Type inheritance
Every token needs a type. If the token does not declare $type, its type is resolved in this order:
- If the value is an alias, the type is that of the referenced token.
- Otherwise, the
$typeof the closest parent group that declares one applies. - Otherwise, the token is invalid.
The specification forbids tools from “guessing” the type by looking at the value. A lone #ffffff is not automatically a color.
$root and $extends
Two group features deserve attention.
$root lets a group have a base token alongside its variations. It is useful for cases such as an accent color with light and dark versions:
{
"color": {
"accent": {
"$root": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.867, 0, 0] }
},
"light": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [1, 0.133, 0.133] }
}
}
}
}
Here, {color.accent.$root} points to the base token. {color.accent}, on the other hand, is invalid, because it references a group.
$extends makes a group inherit tokens from another one, overriding only what changes. Inheritance is a deep merge: a local token completely replaces the inherited one at the same path.
{
"button": {
"$type": "color",
"background": { "$value": { "colorSpace": "srgb", "components": [0, 0.4, 0.8] } },
"text": { "$value": { "colorSpace": "srgb", "components": [1, 1, 1] } }
},
"button-danger": {
"$extends": "{button}",
"background": { "$value": { "colorSpace": "srgb", "components": [0.8, 0, 0.2] } }
}
}
Aliases: tokens that point to tokens
As we saw in the first article, an alias is a token with no value of its own that points to another token. The specification has two syntaxes.
Curly braces, the main syntax, which always point to a whole token:
{
"color": {
"blue": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0, 0.4, 0.8] }
},
"action": { "$value": "{color.blue}" }
}
}
Since color.action does not declare a type, it inherits the color type from the token it references.
JSON Pointer with $ref, for advanced cases. It lets you point to a property inside a value, such as the first component of a color or just the value of a dimension:
{
"space": {
"base": {
"$type": "dimension",
"$value": { "value": 16, "unit": "px" }
},
"base-rem": {
"$type": "dimension",
"$value": {
"value": { "$ref": "#/space/base/$value/value" },
"unit": "rem"
}
}
}
}
Rules that apply to both:
- Chained aliases are allowed (A points to B, which points to C). The tool follows the chain until it finds an explicit value.
- Circular references are an error and invalidate every token in the chain.
- Tools should preserve references and only resolve them when the final value is needed. That is what lets you change a primitive and see the effect on every alias.
Simple types
Every token has a type, and its value must follow the syntax of that type. If it does not, the token is invalid. Think of a typed language: $type declares what is allowed in $value.
color
The value is an object with:
| Property | Required | Description |
|---|---|---|
colorSpace | Yes | Color space |
components | Yes | Array with the components (numbers or the keyword none) |
alpha | No | Opacity from 0 to 1. If omitted, it is 1 |
hex | No | Fallback color in 6 digits |
{
"color-translucent": {
"$type": "color",
"$value": {
"colorSpace": "srgb",
"components": [0, 0, 0],
"alpha": 0.5,
"hex": "#000000"
}
}
}
This changes how we think about color: a string like #3B82F6 is no longer the format of the value. The components are numeric and the hex becomes just a fallback. The benefit is that a token can express a color in any supported color space:
| Space | colorSpace | Components |
|---|---|---|
| sRGB | srgb | Red, green, blue (0 to 1) |
| Linear sRGB | srgb-linear | Red, green, blue (0 to 1) |
| HSL | hsl | Hue (0 to 360, exclusive), saturation (0 to 100), lightness (0 to 100) |
| HWB | hwb | Hue, whiteness, blackness |
| CIELAB | lab | Lightness (0 to 100), a, b |
| LCH | lch | Lightness (0 to 100), chroma, hue |
| OKLAB | oklab | Lightness (0 to 1), a, b |
| OKLCH | oklch | Lightness (0 to 1), chroma, hue |
| Display P3 | display-p3 | Red, green, blue (0 to 1) |
| A98 RGB | a98-rgb | Red, green, blue (0 to 1) |
| ProPhoto RGB | prophoto-rgb | Red, green, blue (0 to 1) |
| Rec 2020 | rec2020 | Red, green, blue (0 to 1) |
| XYZ D65 | xyz-d65 | X, Y, Z |
| XYZ D50 | xyz-d50 | X, Y, Z |
The none keyword (inherited from CSS Color Level 4) indicates that a component is “missing” or does not apply. In a gray in HSL, for example, the hue can be none instead of 0. The two look the same on screen, but they behave differently in interpolation and color mixing.
If you work with colors in OKLCH, the specification lets you write:
{
"color-brand-500": {
"$type": "color",
"$value": {
"colorSpace": "oklch",
"components": [0.62, 0.19, 259]
}
}
}
The hex is optional. If you include it as a fallback, generate it from the real value with a conversion tool rather than by hand.
dimension
Represents a distance on a single axis: position, width, height, radius, thickness. The value is an object with value (a number) and unit, which can only be px or rem: Margin note: em, vh and % are out
{
"space-200": {
"$type": "dimension",
"$value": { "value": 0.5, "unit": "rem" }
},
"space-none": {
"$type": "dimension",
"$value": { "value": 0, "unit": "px" }
}
}
Two things to keep in mind:
- The
unitis required even when the value is0. pxis an “idealized” pixel (the equivalent ofdpon Android andpton iOS).remis a multiple of the system’s default font size, which the user can configure. On platforms with no equivalent torem, the translation tool may convert it to px assuming 16px.
fontFamily
A font name (string) or an array of names, from most to least preferred:
{
"font-family-body": {
"$type": "fontFamily",
"$value": ["Inter", "Helvetica", "Arial", "sans-serif"]
}
}
fontWeight
A number between 1 and 1000 (following the OpenType wght axis) or a predefined name:
| Number | Accepted names |
|---|---|
| 100 | thin, hairline |
| 200 | extra-light, ultra-light |
| 300 | light |
| 400 | normal, regular, book |
| 500 | medium |
| 600 | semi-bold, demi-bold |
| 700 | bold |
| 800 | extra-bold, ultra-bold |
| 900 | black, heavy |
| 950 | extra-black, ultra-black |
Values outside 1 to 1000, or any other names (including ones that differ only by case), are invalid. This supports variable fonts: 350 is a valid value.
{
"font-weight-body": { "$type": "fontWeight", "$value": 400 },
"font-weight-strong": { "$type": "fontWeight", "$value": "bold" }
}
duration
The length of an animation, as an object with value and unit (ms or s):
{
"duration-fast": {
"$type": "duration",
"$value": { "value": 150, "unit": "ms" }
}
}
cubicBezier
The progression curve of an animation (acceleration, deceleration, bounce). It is an array of four numbers [P1x, P1y, P2x, P2y]. The x values are restricted to the range 0 to 1; the y values can be any number.
{
"easing-standard": {
"$type": "cubicBezier",
"$value": [0.4, 0, 0.2, 1]
}
}
number
A plain JSON number. It is used for unitless values, such as gradient stop positions or line height as a multiplier:
{
"line-height-body": { "$type": "number", "$value": 1.5 }
}
What does not exist as a type yet
The specification acknowledges gaps. Font style (normal, italic), percentage/ratio (such as opacity) and files (assets) only appear as types “to be documented”. In the meantime, the most common approach is to use number for values such as opacity and $extensions for specific needs. Treat anything outside what is standardized as your team’s own choice, not as part of the standard.
Composite tokens
Some interface aspects are combinations of several values that always go together. A shadow is made of a color, offsets, blur and spread. A typography style is made of a family, size, weight, letter spacing and line height.
A composite token has an object (or array) with a fixed structure as its value. Each sub-value has a known type and can be explicit or an alias to a token of the right type.
This is different from a group:
| Group | Composite token | |
|---|---|---|
| Is it a token? | No | Yes |
| Can it be referenced by an alias? | No | Yes |
| Structure | Free | Fixed, defined by the type |
| Can it have extra sub-values? | Yes | No: it makes the token invalid |
strokeStyle
The stroke style of lines and borders. It accepts one of two forms, which are mutually exclusive:
String, with values equivalent to CSS: solid, dashed, dotted, double, groove, ridge, outset, inset.
{
"stroke-focus": { "$type": "strokeStyle", "$value": "dashed" }
}
Object, with dashArray (a list of dimensions alternating dash and gap) and lineCap (round, butt or square):
{
"stroke-alert": {
"$type": "strokeStyle",
"$value": {
"dashArray": [
{ "value": 0.5, "unit": "rem" },
{ "value": 0.25, "unit": "rem" }
],
"lineCap": "round"
}
}
}
Since not every platform handles every style (CSS does not let you control the dash pattern, and Figma does not support inset, outset or double), tools should fall back to the closest approximation they support.
border
An object with color, width and style (a strokeStyle):
{
"border-subtle": {
"$type": "border",
"$value": {
"color": "{color.border.subtle}",
"width": { "value": 1, "unit": "px" },
"style": "solid"
}
}
}
transition
An object with duration, delay and timingFunction (a cubicBezier):
{
"transition-emphasis": {
"$type": "transition",
"$value": {
"duration": { "value": 200, "unit": "ms" },
"delay": { "value": 0, "unit": "ms" },
"timingFunction": [0.5, 0, 1, 1]
}
}
}
The specification itself raises an open question: these parameters alone do not say which property is animated, nor the start and end states.
shadow
An object with color, offsetX, offsetY, blur, spread and, optionally, inset (an inner shadow, false by default). The value can be an object or an array of shadows, which allows layered shadows:
{
"shadow-medium": {
"$type": "shadow",
"$value": {
"color": "{color.shadow-050}",
"offsetX": "{space.small}",
"offsetY": "{space.small}",
"blur": { "value": 1.5, "unit": "rem" },
"spread": { "value": 0, "unit": "rem" }
}
},
"shadow-elevated": {
"$type": "shadow",
"$value": [
"{shadow-medium}",
{
"color": { "colorSpace": "srgb", "components": [0, 0, 0], "alpha": 0.2 },
"offsetX": { "value": 0, "unit": "px" },
"offsetY": { "value": 24, "unit": "px" },
"blur": { "value": 44, "unit": "px" },
"spread": { "value": 0, "unit": "px" }
}
]
}
}
In the array, each element can be an explicit shadow or a reference to another shadow token. References resolve to a single object and are not “flattened” into the array.
gradient
An array of stops, each with a color and a position. The position is a number from 0 (start) to 1 (end); outside that range, the value is clamped to it. If there is no stop at 0 or 1, the color of the nearest stop extends to the edge.
{
"gradient-brand": {
"$type": "gradient",
"$value": [
{ "color": "{color.brand.500}", "position": 0 },
{ "color": "{color.brand.700}", "position": 1 }
]
}
}
The specification itself asks whether the type should indicate the gradient shape (linear, radial, conic). Today, this type only describes the stops.
typography
An object with fontFamily, fontSize, fontWeight, letterSpacing and lineHeight. Each property is a value of the corresponding type or an alias. The lineHeight is a number, interpreted as a multiplier of fontSize.
{
"text-heading-1": {
"$type": "typography",
"$value": {
"fontFamily": "{font.family.heading}",
"fontSize": { "value": 2.5, "unit": "rem" },
"fontWeight": 700,
"letterSpacing": { "value": 0, "unit": "px" },
"lineHeight": 1.2
}
}
}
You can also reuse parts of one typography style in another, using $ref to point only to the fontFamily or the lineHeight of a base token.
Types at a glance
| Type | Value | Token kind |
|---|---|---|
color | Object with colorSpace, components, alpha, hex | Simple |
dimension | { value, unit } with px or rem | Simple |
fontFamily | String or array of strings | Simple |
fontWeight | Number from 1 to 1000 or a predefined name | Simple |
duration | { value, unit } with ms or s | Simple |
cubicBezier | Array of 4 numbers | Simple |
number | JSON number | Simple |
strokeStyle | Predefined string or { dashArray, lineCap } | Composite |
border | { color, width, style } | Composite |
transition | { duration, delay, timingFunction } | Composite |
shadow | Object or array of { color, offsetX, offsetY, blur, spread, inset } | Composite |
gradient | Array of { color, position } | Composite |
typography | { fontFamily, fontSize, fontWeight, letterSpacing, lineHeight } | Composite |
What this changes in practice
1. Mind the format version. Earlier drafts of the specification (and many examples on the internet) wrote colors as "#3B82F6" and dimensions as "16px". Version 2025.10 uses objects. If you have old files, you will need to migrate them. Before adopting the new format, check the documentation of your translation tool and design plugin to see which version of the format it reads. The tool may require a specific version or a setting for it.
2. Types matter for translation. It is $type that lets a tool turn a dimension into rem for the web and pt for iOS, or an OKLCH color into a hexadecimal fallback.
3. Not everything fits a composite type. If your system needs a font style, an opacity with its own name or an asset, you will use number, aliases and $extensions. Document these decisions, because they are not interoperable by default.
4. Composite or separate? You can represent typography as a single typography token or as five simple tokens (font-size, font-weight, and so on). The composite keeps the properties together and simplifies use in design tools. Separate tokens are more flexible to consume in code. Many teams keep both. It is a decision that depends on the platforms and tools you use.
This is the second article of the Ultimate Guide to Design Tokens. In the next one, we will talk about token naming: how to name, structure and organize tokens so the system stays readable as it grows.
So you don’t miss what comes next, subscribe to the newsletter and follow me on LinkedIn.