Skip to content

Custom Outputs

Build a custom output when your target format is not covered by the built-in builders (css, json, js, tailwind, ios, android). Examples: YAML, SCSS variables, Android XML resources, custom config formats.

A custom output has two parts:

  1. A renderer — a format function that converts tokens into your target format
  2. An output config — wires the renderer into the build with a name, file path, transforms, filters, and options

Every output in dispersa.build() is an OutputConfig object. Built-in builders like css() and json() create these for you. For a custom output, you create one directly:

import { Dispersa } from 'dispersa'
import { nameKebabCase } from 'dispersa/transforms'
import { byType } from 'dispersa/filters'
const dispersa = new Dispersa({ resolver: './tokens.resolver.json' })
await dispersa.build({
outputs: [{
name: 'yaml',
renderer: yamlRenderer,
file: 'tokens.yaml',
options: { header: 'Design Tokens' },
transforms: [nameKebabCase()],
filters: [byType('color')],
}],
})
PropertyTypeDescription
namestringUnique identifier for this output
rendererRendererThe renderer that formats tokens (see below)
filestring | FileFunctionOutput path; supports modifier key placeholders like {theme}, {density}
optionsRecord<string, unknown>Renderer-specific options passed to format()
transformsTransform[]Per-output transforms (run after global transforms)
filtersFilter[]Per-output filters (run after global filters)
hooksLifecycleHooksPer-output onBuildStart and onBuildEnd hooks

Use defineRenderer<T>() to create a type-safe renderer. The generic parameter types the options argument:

import { defineRenderer } from 'dispersa'
type YamlOptions = {
header?: string
}
const yamlRenderer = defineRenderer<YamlOptions>({
format(context, options) {
const tokens = context.permutations[0]?.tokens ?? {}
const header = options?.header ? `# ${options.header}\n\n` : ''
const lines = Object.entries(tokens)
.map(([name, token]) => `${name}: ${JSON.stringify(token.$value)}`)
.join('\n')
return `${header}${lines}\n`
},
})

Your format function receives a RenderContext:

PropertyDescription
permutationsArray of { tokens, modifierInputs } — one entry per permutation (modifier combination)
outputThe output config (file, name, etc.)
resolverThe resolver document
metaRenderMeta: dimensions, defaults, basePermutation
buildPathOutput directory (when building to disk)

context.meta provides:

  • dimensions — Names of modifier dimensions (e.g. ['theme', 'density'])
  • defaults — Default values per dimension
  • basePermutation — The base (unmodified) permutation values

Use basePermutation to identify which permutation is the base when working with presets.

Return multiple files by wrapping a record in outputTree():

import { defineRenderer, outputTree } from 'dispersa'
const multiFileRenderer = defineRenderer({
format(context) {
const files: Record<string, string> = {}
for (const { tokens, modifierInputs } of context.permutations) {
const key = Object.values(modifierInputs).join('-') || 'default'
const content = Object.entries(tokens)
.map(([name, token]) => `${name}: ${JSON.stringify(token.$value)}`)
.join('\n')
files[`tokens-${key}.yaml`] = content
}
return outputTree(files)
},
})

Each key in the record becomes a file path; each value is the file content.

const scssRenderer = defineRenderer({
format(context) {
const tokens = context.permutations[0]?.tokens ?? {}
const lines = Object.entries(tokens)
.map(([name, token]) => `$${name.replace(/\./g, '-')}: ${token.$value};`)
.join('\n')
return lines + '\n'
},
})
// Wire into a build
await dispersa.build({
outputs: [{
name: 'scss',
renderer: scssRenderer,
file: 'variables.scss',
transforms: [nameKebabCase()],
}],
})
const androidXmlRenderer = defineRenderer({
format(context) {
const tokens = context.permutations[0]?.tokens ?? {}
const items = Object.entries(tokens)
.map(([name, token]) => {
const attr = token.$type === 'color' ? 'android:color' : 'android:dimen'
return ` <item name="${name.replace(/\./g, '_')}" ${attr}="${token.$value}" />`
})
.join('\n')
return `<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${items}\n</resources>\n`
},
})