Brand tokens export
@brandkit/brand-tokens-export
Turn the tokens in your brand.md into code: CSS variables, a Tailwind theme (v3 and v4), and a design-tokens.json for Figma token plugins. One source of truth, regenerated in a second whenever the brand changes.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1#!/usr/bin/env node
- 2// Turns the tokens block in brand.md into CSS variables, a Tailwind theme, and design-tokens.json.
- 3// Usage: node scripts/export-tokens.mjs [brand.md] [out-dir] [--prefix ""]
- 4// Writes: <out-dir>/tokens.css, <out-dir>/theme.css (Tailwind v4 @theme),
- 5// <out-dir>/tailwind.theme.cjs (Tailwind v3), <out-dir>/design-tokens.json
- 6import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
- 7import { join } from "node:path";
- 8import { flatten, readTokens, slug } from "./tokens-lib.mjs";
- 10const args = process.argv.slice(2);
- 11function flag(name, fallback) {
- 12 const i = args.indexOf(name);
- 13 if (i === -1) return fallback;
- 14 const value = args[i + 1] ?? "";
- 15 args.splice(i, 2);
- 16 return value;
- 17}
- 18const prefix = flag("--prefix", "");
- 19const [input = "brand.md", outDir = "brand-tokens"] = args;
- 21if (!existsSync(input)) {
- 22 console.error(`export-tokens: ${input} not found. Pass the path to your brand guide, for example docs/brand.md.`);
- 23 process.exit(1);
- 24}
- 26let tokens;
- 27try {
- 28 tokens = readTokens(readFileSync(input, "utf8"));
- 29} catch (err) {
- 30 console.error(`export-tokens: ${input}: ${err.message}`);
- 31 process.exit(1);
- 32}
- 34// Group in brand.md -> CSS variable namespace (Tailwind v4 names), Tailwind v3 key, DTCG type.
- 35const GROUPS = [
- 36 { group: "color", css: "color", tw: "colors", type: "color" },
- 37 { group: "font", css: "font", tw: "fontFamily", type: "fontFamily" },
- 38 { group: "text", css: "text", tw: "fontSize", type: "dimension" },
- 39 { group: "weight", css: "font-weight", tw: "fontWeight", type: "fontWeight" },
- 40 { group: "space", css: "spacing", tw: "spacing", type: "dimension" },
- 41 { group: "radius", css: "radius", tw: "borderRadius", type: "dimension" },
- 42 { group: "shadow", css: "shadow", tw: "boxShadow", type: null },
- 43];
- 45const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
- 46const warnings = [];
- 47const vars = [];
- 48const tailwind = {};
- 49const dtcg = {};
- 51function fontList(value) {
- 52 return String(value)
- 53 .split(",")
- 54 .map((s) => s.trim())
- 55 .filter(Boolean);
- 56}
- 57function cssFont(value) {
- 58 return fontList(value)
- 59 .map((f) => (/\s/.test(f) && !/^["']/.test(f) ? `"${f}"` : f))
- 60 .join(", ");
- 61}
- 63for (const g of GROUPS) {
- 64 const entries = flatten(tokens[g.group]);
- 65 if (entries.length === 0) continue;
- 66 tailwind[g.tw] = {};
- 67 dtcg[g.group] = {};
- 68 for (const [name, value] of entries) {
- 69 const key = slug(name);
- 70 if (g.group === "color" && !HEX.test(value) && !/^(rgb|hsl|oklch)a?\(/i.test(value)) {
- 71 warnings.push(`color.${name} "${value}" is not a hex, rgb, hsl, or oklch color`);
- 72 }
- 73 const cssValue = g.group === "font" ? cssFont(value) : value;
- 74 vars.push([`--${prefix}${g.css}-${key}`, cssValue]);
- 75 tailwind[g.tw][key] = g.group === "font" ? fontList(value) : value;
- 76 const token = { $value: g.group === "font" ? fontList(value) : value };
- 77 if (g.type) token.$type = g.type;
- 78 dtcg[g.group][key] = token;
- 79 }
- 80}
- 82if (vars.length === 0) {
- 83 console.error(
- 84 `export-tokens: ${input}: the tokens block has no color, font, text, weight, space, radius, or shadow groups.`,
- 85 );
- 86 process.exit(1);
- 87}
- 89const name = tokens.name ? String(tokens.name) : "Brand";
- 90const header = `/* ${name} design tokens. Generated from ${input} by brand-tokens-export. Edit brand.md, not this file. */`;
- 91const body = vars.map(([k, v]) => ` ${k}: ${v};`).join("\n");
- 92const tokensCss = `${header}\n:root {\n${body}\n}\n`;
- 93const themeCss = `${header}\n/* Tailwind v4: import after "tailwindcss" in your main CSS file. */\n@theme {\n${body}\n}\n`;
- 94const requirePath = outDir.startsWith("/") ? outDir : `./${outDir.replace(/^\.\//, "").replace(/\/$/, "")}`;
- 95const twJs = `// ${name} theme for Tailwind v3. Generated from ${input} by brand-tokens-export.\n// In tailwind.config.js: theme: { extend: require("${requirePath}/tailwind.theme.cjs") }\nmodule.exports = ${JSON.stringify(tailwind, null, 2)};\n`;
- 96const json = { $description: `${name} design tokens (W3C design tokens format)`, ...dtcg };
- 98mkdirSync(outDir, { recursive: true });
- 99const files = {
- 100 "tokens.css": tokensCss,
- 101 "theme.css": themeCss,
- 102 "tailwind.theme.cjs": twJs,
- 103 "design-tokens.json": `${JSON.stringify(json, null, 2)}\n`,
- 104};
- 105for (const [file, content] of Object.entries(files)) writeFileSync(join(outDir, file), content);
- 107console.log(`Exported ${vars.length} tokens from ${input} to ${outDir}/`);
- 108for (const file of Object.keys(files)) console.log(` ${join(outDir, file)}`);
- 109for (const w of warnings) console.log(` warning: ${w}`);
