Custom Outputs
When to Build a Custom Output
Section titled “When to Build a Custom Output”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:
- A renderer — a
formatfunction that converts tokens into your target format - An output config — wires the renderer into the build with a name, file path, transforms, filters, and options
The Output Config
Section titled “The Output Config”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')], }],})| Property | Type | Description |
|---|---|---|
| name | string | Unique identifier for this output |
| renderer | Renderer | The renderer that formats tokens (see below) |
| file | string | FileFunction | Output path; supports modifier key placeholders like {theme}, {density} |
| options | Record<string, unknown> | Renderer-specific options passed to format() |
| transforms | Transform[] | Per-output transforms (run after global transforms) |
| filters | Filter[] | Per-output filters (run after global filters) |
| hooks | LifecycleHooks | Per-output onBuildStart and onBuildEnd hooks |
Creating a Renderer with defineRenderer
Section titled “Creating a Renderer with defineRenderer”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` },})RenderContext
Section titled “RenderContext”Your format function receives a RenderContext:
| Property | Description |
|---|---|
| permutations | Array of { tokens, modifierInputs } — one entry per permutation (modifier combination) |
| output | The output config (file, name, etc.) |
| resolver | The resolver document |
| meta | RenderMeta: dimensions, defaults, basePermutation |
| buildPath | Output directory (when building to disk) |
RenderMeta
Section titled “RenderMeta”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.
Multi-File Output with outputTree()
Section titled “Multi-File Output with outputTree()”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.
Example: SCSS Variables
Section titled “Example: SCSS Variables”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 buildawait dispersa.build({ outputs: [{ name: 'scss', renderer: scssRenderer, file: 'variables.scss', transforms: [nameKebabCase()], }],})Example: Android XML Resources
Section titled “Example: Android XML Resources”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` },})