Skip to content

Your First Build

If you ran pnpm create dispersa and chose the TypeScript Starter template, you already have a working project. Let’s explore what you got and run the build.

The scaffold creates this structure:

my-dispersa-project/
├── tokens/
│ ├── base/
│ │ ├── colors.json # Foundational color palette
│ │ ├── spacing.json # Spacing scale
│ │ ├── typography.json # Font families, sizes, weights
│ │ └── effects.json # Shadows and effects
│ ├── alias/
│ │ ├── colors.json # Semantic color aliases (text, background, action, …)
│ │ ├── typography.json # Semantic typography (heading, body)
│ │ ├── spacing.json # Semantic spacing (gap, inset)
│ │ └── effects.json # Semantic effects (elevation)
│ └── themes/
│ ├── light.json # Light theme overrides
│ └── dark.json # Dark theme overrides
├── tokens.resolver.json # DTCG resolver -- ties everything together
├── build.ts # Build script
├── tsconfig.json
└── package.json

tokens/base/colors.json defines your foundational color palette:

{
"color": {
"palette": {
"blue-500": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0, 0.4, 0.8] }
},
"white": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [1, 1, 1] }
},
"gray-900": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.1, 0.11, 0.12] }
}
}
}
}

tokens/alias/colors.json creates purpose-driven names that reference the base tokens. Names follow the six-layer convention: category.concept.sentiment.prominence.state.scale (layers can be omitted when not needed):

{
"color": {
"text": {
"$type": "color",
"default": {
"$value": "{color.palette.gray-900}"
},
"muted": {
"$root": { "$value": "{color.palette.gray-500}" }
}
},
"background": {
"$type": "color",
"default": {
"$value": "{color.palette.white}"
}
},
"action": {
"$type": "color",
"brand": {
"$root": { "$value": "{color.palette.blue-500}" }
}
}
}
}

tokens.resolver.json is the DTCG resolver that tells Dispersa which token files to load and how modifiers (like themes) are applied:

{
"version": "2025.10",
"sets": {
"colors": {
"sources": [{ "$ref": "./tokens/base/colors.json" }, { "$ref": "./tokens/alias/colors.json" }]
},
"typography": {
"sources": [
{ "$ref": "./tokens/base/typography.json" },
{ "$ref": "./tokens/alias/typography.json" }
]
},
"spacing": {
"sources": [
{ "$ref": "./tokens/base/spacing.json" },
{ "$ref": "./tokens/alias/spacing.json" }
]
},
"effects": {
"sources": [
{ "$ref": "./tokens/base/effects.json" },
{ "$ref": "./tokens/alias/effects.json" }
]
}
},
"modifiers": {
"theme": {
"default": "light",
"contexts": {
"light": [{ "$ref": "./tokens/themes/light.json" }],
"dark": [{ "$ref": "./tokens/themes/dark.json" }]
}
}
},
"resolutionOrder": [
{ "$ref": "#/sets/colors" },
{ "$ref": "#/sets/typography" },
{ "$ref": "#/sets/spacing" },
{ "$ref": "#/sets/effects" },
{ "$ref": "#/modifiers/theme" }
]
}

build.ts creates a Dispersa instance, points it at the resolver, and defines CSS output with transforms:

import { build, css } from 'dispersa'
import { colorToHex } from 'dispersa/transforms'
const result = await build({
resolver: './tokens.resolver.json',
buildPath: './output',
outputs: [
css({
name: 'css',
file: 'tokens.css',
preset: 'bundle',
preserveReferences: true,
transforms: [colorToHex()],
}),
],
})
if (result.success) {
console.log('Build successful!')
}
Terminal window
pnpm run build

This executes tsx build.ts, which resolves your tokens, applies transforms, and writes the output.

Open output/tokens.css. You should see your base tokens and theme overrides rendered as CSS custom properties:

:root {
--color-palette-blue-500: #0066cc;
--color-palette-white: #ffffff;
--color-palette-gray-900: #1a1c1f;
--color-text-default: var(--color-palette-gray-900);
--color-text-muted: var(--color-palette-gray-500);
--color-background-default: var(--color-palette-white);
--color-action-brand: var(--color-palette-blue-500);
/* ... */
}
[data-theme='dark'] {
--color-text-default: var(--color-palette-gray-100);
--color-background-default: var(--color-palette-gray-900);
}

Because preserveReferences is true, semantic tokens emit var() references to their base tokens instead of inlining the resolved hex value. The bundle preset puts the base values in :root and the dark theme overrides in a [data-theme="dark"] selector — all in a single file.

From here you can:

  • Edit tokens — add new values to tokens/base/colors.json or create new token files and reference them in the resolver
  • Add transforms — e.g. dimensionToPx() or colorToRgb() from dispersa/transforms
  • Add outputs — e.g. json() or js() alongside css()
  • Switch to the CLI — replace build.ts with a dispersa.config.ts and run dispersa build