Skip to content

@tabula-css/tokens

Tabula's flat-DTCG-profile parser, value-semantics validator, axis resolver, and emitters.

Install

@tabula-css/tokens is not part of the quickstart's direct installs — it is a transitive dependency of @tabula-css/registry (and, through it, @tabula-css/cli), which is what most projects use to turn tokens into a build. Install it directly only if you are building custom tooling around the token pipeline itself — for example, a script that validates a token file or resolves it to CSS without running a full tabula build:

bash
npm install @tabula-css/tokens

Overview

This is where a design token stops being JSON and becomes a value the rest of the system can trust. @tabula-css/tokens implements steps 1–5 of the build pipeline described in Getting started: parse the token and config files, validate value semantics (not just shape — a { value: 0, unit: "px" } font-size is well-formed JSON and a build error here), resolve axis maps and aliases into a literal matrix, and emit the four W1 artifacts (theme.css, tokens.resolved.json, vocabulary.txt, types.d.ts). The resolved model it produces — the synthesized vocabulary, its expected declarations, the custom-property table — is exactly what @tabula-css/registry consumes to derive registry.json from Tailwind's real compiled output.

Exports

buildProfile()

ts
export interface BuildResult {
  readonly ok: boolean;
  readonly diagnostics: readonly Diagnostic[];
  readonly model?: ResolvedModel;   // present only when validation passed
  readonly artifacts?: Artifacts;   // present only when validation passed
}

export function buildProfile(
  tokensJson: string,
  configJson: string,
  options?: ValidateOptions,
): BuildResult;

Runs the entire W1 slice end to end — parse, validate, resolve, emit — and fails closed: on any error-severity diagnostic, no model and no artifacts are returned. This is the single function to call if you just want "tokens + config in, artifacts or diagnostics out."

ts
import { readFileSync } from 'node:fs';
import { buildProfile } from '@tabula-css/tokens';

const result = buildProfile(
  readFileSync('tokens/base.tokens.json', 'utf8'),
  readFileSync('tabula.config.json', 'utf8'),
);
if (!result.ok) {
  for (const d of result.diagnostics) console.error(`${d.code}: ${d.message}`);
  process.exit(1);
}
console.log(result.artifacts['vocabulary.txt']);

parseTokens()

ts
export interface ParseResult {
  readonly ast: TokensFile | null;
  readonly diagnostics: readonly Diagnostic[];
}
export function parseTokens(json: string): ParseResult;

Parses a token-file string into an AST. This stage only guarantees the input is RFC 8259 JSON and a top-level object — no JSON5, comments, trailing commas, or a leading BOM — returning TAB-E101 and ast: null otherwise. Value semantics are validated separately by validateTokens().

validateConfig() / parseConfig()

ts
export interface ConfigResult {
  readonly ok: boolean;
  readonly config: TabulaConfig | null;
  readonly diagnostics: readonly Diagnostic[];
}
export function validateConfig(input: unknown): ConfigResult;
export function parseConfig(json: string): ConfigResult;

Validate an already-parsed config object (validateConfig) or a raw JSON string (parseConfig, which also reports TAB-E101 on invalid JSON). Both check tabula.config.json structurally in code — no JSON Schema engine at runtime, so this package's only dependency is @tabula-css/core — reporting axis problems as TAB-E120 and an unsafe dynamicProperties[*].syntax (the universal '*', which would validate nothing) as TAB-E127.

validateTokens()

ts
export interface ValidateOptions {
  /** Reference "now" for expiry/removeAfter checks. Defaults to the current date. */
  readonly now?: Date;
}
export interface ValidationResult {
  readonly ok: boolean;
  readonly diagnostics: readonly Diagnostic[];
}
export function validateTokens(
  ast: TokensFile,
  config: TabulaConfig,
  options?: ValidateOptions,
): ValidationResult;

The schema-and-value-semantics gate. Runs after parseTokens() and performs every structural check (P1–P15: depth-2 nesting, known namespaces, kebab token names, required $description ≥ 20 chars, forbidden $ref, alias depth ≤ 1, …) and every value-semantics pass from Concepts: axis totality (TAB-E113 — every declared axis member needs a literal, with no fallback), contrastWith across every axis combination (TAB-E153), per-namespace dimension domains (TAB-E111, TAB-E155E157), duration/opacity/fontWeight/z domains, the type/shadow composites (TAB-E162/E163), the alias graph, exception paperwork (TAB-E140/E141 — reason ≥ 40 chars and not boilerplate, ISO expiry ≤ 12 months out), and class-name uniqueness (TAB-E112).

ts
import { parseTokens, parseConfig, validateTokens } from '@tabula-css/tokens';

const { ast } = parseTokens(tokensJson);
const { config } = parseConfig(configJson);
const { ok, diagnostics } = validateTokens(ast, config, { now: new Date('2026-07-22') });

resolveTokens() and the axis helpers

ts
export function resolveTokens(ast: TokensFile, config: TabulaConfig): ResolvedModel;

export function axisSignature(value: unknown, ast: TokensFile, depth?: number): readonly string[];
export function enumerateCombos(axes: readonly string[], config: TabulaConfig): AxisState[];
export function defaultCombo(axes: readonly string[], config: TabulaConfig): AxisState;
export function resolveRaw(value: unknown, combo: AxisState, ast: TokensFile, depth?: number): unknown;
export function resolveLiteral(token: Token, combo: AxisState, ast: TokensFile): ResolvedLiteral;

resolveTokens() is the axis resolver and vocabulary synthesizer: it follows every alias, expands every axis map to its full literal matrix, and synthesizes the candidate vocabulary (namespace × family × token, plus STATIC_UTILITIES and declared exceptions) with each candidate's expected declarations, slots, and rank. It assumes the AST already passed validateTokens(). axisSignature() reports which axes a token's value varies over; enumerateCombos()/defaultCombo() produce the axis-combination cross-product and its default member; resolveRaw()/resolveLiteral() resolve a raw $value (or a whole token) at one specific combination.

ts
export interface ResolvedModel {
  readonly profileId: string;
  readonly config: TabulaConfig;
  readonly tokens: readonly ResolvedToken[];
  /** The synthesized vocabulary, in ascending rank order. */
  readonly candidates: readonly CandidateClass[];
  readonly customProperties: readonly CustomPropertyDef[];
  /** Declared variant products (contract J22): chain prefix → sorted family names, `"*"` pre-expanded. */
  readonly variantProducts: Readonly<Record<string, readonly string[]>>;
  /** The materialized variant chains, sorted ascending by `(effectiveRank, class)`. */
  readonly chainCandidates: readonly ChainCandidate[];
  readonly diagnostics: readonly Diagnostic[];
}

export interface ChainCandidate {
  /** Full canonical class, e.g. `"hover:bg-accent-hover"`. */
  readonly class: string;
  /** Canonical ascending-rank, colon-joined chain prefix, no trailing colon: `"hover"` | `"sm:hover"`. */
  readonly chainPrefix: string;
  /** The base utility class name the prefix is applied to. */
  readonly utility: string;
  /** Σ of the chain's variant ranks. */
  readonly conditionRank: number;
  /** `conditionRank * CONDITION_RANK_SCALE + base class rank` — the merge's total order. */
  readonly effectiveRank: number;
  /** Exception paperwork, present only for an exact-class chain grant (`except add --chain`). */
  readonly exception?: ExceptionMeta;
}

The variants closure shipped in v0.2: resolveTokens() reads the config's declared variant products, expands each chainPrefix × family product into a ChainCandidate, and emits them into chainCandidates in (effectiveRank, class) order. @tabula-css/preset's buildChainLayer() and @tabula-css/registry's generator read that array verbatim — the preset to emit one literal CSS rule per chain, the generator to record variantProducts/chainCount in registry.json.

Emitters

ts
export function emitThemeCss(model: ResolvedModel): string;
export function emitResolvedJson(model: ResolvedModel): string;
export function emitVocabulary(model: ResolvedModel): string;
export function emitTypes(model: ResolvedModel): string;

export function ambientBaselineDecls(model: ResolvedModel): { decls: Array<[string, string]>; missing: string[] };
export function checkAmbientBaseline(model: ResolvedModel): Diagnostic[];

Four pure ResolvedModel → string functions, each corresponding to one generated artifact: emitThemeCss (@property declarations plus @layer tabula.tokens root/attribute/media axis blocks), emitResolvedJson (the flat, fully-literal tokens.resolved.json), emitVocabulary (one legal class per line, in rank order), and emitTypes (the TabulaUtility/TabulaVariant/TabulaClass union types in types.d.ts).

As of v0.2, emitResolvedJson preserves each token's provenance markers verbatim — $deprecated, $extensions (including a foreign vendor namespace like com.example.figma), and a $alias marker recording the original dot-path when the source $value was a single top-level alias — so a committed .tabula/ can reconstruct the source profile it was built from. See Migration § Restoring a profile from .tabula/.

ambientBaselineDecls() computes the base-level ambient baseline (every inheritable profile-owned property emitted once at :root, per Concepts § profile levels) and reports anything that couldn't be sourced. checkAmbientBaseline() turns a non-empty missing list into TAB-E220 error diagnostics — a no-op under strict, whose typography closure makes the baseline redundant.

Colour

ts
export interface Rgb { readonly r: number; readonly g: number; readonly b: number; readonly a: number; }
export type ColorParse =
  | { readonly ok: true; readonly srgb: true; readonly rgb: Rgb }
  | { readonly ok: true; readonly srgb: false }  // valid CSS colour, not sRGB-resolvable here
  | { readonly ok: false };

export function parseColor(input: string): ColorParse;
export function relativeLuminance(c: Rgb): number;
export function contrastRatio(a: Rgb, b: Rgb): number;

A small, self-contained CSS colour parser (hex, rgb()/rgba(), hsl()/hsla(), a common set of named colours, transparent) plus WCAG 2.x relative luminance and contrast ratio, used by validateTokens()'s colour-domain and contrastWith checks. Wide-gamut functions (oklch(), oklab(), lab(), lch(), color()) parse as syntactically valid (srgb: false) but are not resolved to sRGB channels — which is what lets the validator distinguish "not a colour at all" (TAB-E150) from "a valid colour whose sRGB-gamut membership cannot be confirmed" (TAB-E151).

ts
import { parseColor, contrastRatio } from '@tabula-css/tokens';

const surface = parseColor('#0b0b0c');
const text = parseColor('#ffffff');
if (surface.ok && surface.srgb && text.ok && text.srgb) {
  contrastRatio(surface.rgb, text.rgb); // ≥ 1, WCAG-comparable
}

See also

  • @tabula-css/core — the frozen tables and error catalog validateTokens()/resolveTokens() check against.
  • @tabula-css/registry — consumes ResolvedModel to derive registry.json from Tailwind's compiled CSS.
  • @tabula-css/preset — consumes ResolvedModel (and emitThemeCss()) to assemble the Tailwind entry stylesheet.
  • Getting started — the full token-authoring walkthrough.
  • Concepts — locality, theming axes, and profile levels.

Released under the MIT License.