February 23, 2026

CSS vars to TS vars

Code following make Javascript and Typescript file declaration to store design tokens callable in Frontend Project

*Code generated by ChatGPT. I didn't check if it work accuracy, but it work for me for now.

Install dependency

  • postcss

Convert to JS

import fs from "node:fs";
import path from "node:path";
import postcss from "postcss";

/** --kebab-case → camelCase */
function kebabToCamel(cssVar) {
  return cssVar
    .replace(/^--/, "")
    .replace(/-([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
}

/**
 * Extract CSS var declarations from file
 */
function extractCssVars(filePath, selectors = [":root"]) {
  const css = fs.readFileSync(filePath, "utf8");
  const root = postcss.parse(css);
  /** @type {string[]} */
  const names = [];

  root.walkRules((rule) => {
    const sels = rule.selectors || [];
    if (!sels.some((s) => selectors.includes(s))) return;
    rule.walkDecls((decl) => {
      if (decl.prop && decl.prop.startsWith("--")) {
        names.push(decl.prop);
      }
    });
  });

  return names;
}

async function run({
  input = "styles/tokens.css",
  outFile = "src/styles/tokens.js",
  selectors = [":root"],
  allowlist = null,
} = {}) {
  const inPath = path.resolve(process.cwd(), input);
  const outPath = path.resolve(process.cwd(), outFile);

  if (!fs.existsSync(inPath)) {
    console.error(`❌ Input CSS not found: ${inPath}`);
    process.exit(1);
  }

  let vars = extractCssVars(inPath, selectors);

  if (allowlist instanceof RegExp) {
    vars = vars.filter((v) => allowlist.test(v));
  }

  // remove duplicates, sort
  vars = Array.from(new Set(vars)).sort();

  const header =
`/* AUTO-GENERATED FILE. DO NOT EDIT.
 * Source: ${path.relative(process.cwd(), inPath)}
 * Run: npm run gen:css-js
 */
`;

  const lines = vars.map((v) => {
    const camel = kebabToCamel(v);
    return `export const ${camel} = 'var(${v})'`;
  });

  const content = `${header}\n${lines.join("\n")}\n`;

  fs.mkdirSync(path.dirname(outPath), { recursive: true });
  fs.writeFileSync(outPath, content, "utf8");

  console.log(`✅ Generated ${path.relative(process.cwd(), outPath)} with ${vars.length} vars.`);
}

run().catch((e) => {
  console.error(e);
  process.exit(1);
});

Example Result:

...
export const spacing720px = 'var(--spacing-720px)'
export const spacing768px = 'var(--spacing-768px)'
export const spacing80px = 'var(--spacing-80px)'
export const spacing8px = 'var(--spacing-8px)'
export const spacing96px = 'var(--spacing-96px)'
export const stroke = 'var(--stroke)'
...

Convert to d.ts

import fs from "node:fs";
import path from "node:path";
import postcss from "postcss";

/** --kebab-case -> camelCase (ลบเครื่องหมาย -- และแปลง -x เป็น X) */
function kebabToCamel(cssVar) {
  return cssVar
    .replace(/^--/, "")
    .replace(/-([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
}

/**
 * อ่านไฟล์ CSS แล้วดึงรายชื่อ CSS custom properties (เริ่มด้วย --)
 * เฉพาะกฎ (rule) ที่ selectors match ตามที่กำหนด
 */
function extractCssVarNamesFromFile(filePath, matchSelectors = [":root"]) {
  const css = fs.readFileSync(filePath, "utf8");
  const root = postcss.parse(css);
  /** @type {string[]} */
  const names = [];

  root.walkRules((rule) => {
    const sels = rule.selectors || [];
    const matched = sels.some((s) => matchSelectors.includes(s));
    if (!matched) return;

    rule.walkDecls((decl) => {
      if (decl.prop && decl.prop.startsWith("--")) {
        names.push(decl.prop);
      }
    });
  });

  return names;
}

/** main */
async function run({
  input = "styles/tokens.css",
  outDts = "src/styles/tokens.d.ts",
  selectors = [":root"], // ใส่ selector เพิ่มได้ เช่น '[data-theme="light"]'
  allowlist = null, // เช่น /^--body-/ ถ้าอยากกรองเฉพาะ prefix
} = {}) {
  const inPath = path.resolve(process.cwd(), input);
  const outPath = path.resolve(process.cwd(), outDts);

  if (!fs.existsSync(inPath)) {
    console.error(`❌ Input CSS not found: ${inPath}`);
    process.exit(1);
  }

  let varNames = extractCssVarNamesFromFile(inPath, selectors);

  // กรองด้วย allowlist (ถ้ากำหนด)
  if (allowlist instanceof RegExp) {
    varNames = varNames.filter((n) => allowlist.test(n));
  }

  // แปลงเป็น camelCase + unique + sort
  const camelNames = Array.from(
    new Set(varNames.map(kebabToCamel))
  ).sort((a, b) => a.localeCompare(b));

  // สร้างเนื้อหา .d.ts
  const header =
`/* AUTO-GENERATED. DO NOT EDIT.
 * Source: ${path.relative(process.cwd(), inPath)}
 * Run: npm run gen:css-dts
 */
`;

  const lines = camelNames.map((name) => `export const ${name}: string`).join("\n");

  const content = `${header}${lines}\n`;

  // เขียนไฟล์
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
  fs.writeFileSync(outPath, content, "utf8");

  console.log(`✅ Generated ${path.relative(process.cwd(), outPath)} with ${camelNames.length} exports.`);
}

run().catch((e) => {
  console.error(e);
  process.exit(1);
});

Example Result

...
export const spacing720px: string
export const spacing768px: string
export const spacing80px: string
export const spacing8px: string
export const spacing96px: string
export const stroke: string
...