//cssforgebyHebilicious

cssforge

Generate CSS variables from design tokens.

1
0
1
1
TypeScript

CSS Forge

0 runtime design tokens generator for modern style systems.

[!WARNING]
CSSForge is an experimental library and its API will change while I figure out a
schema that makes sense. That being said, when the schema change, you should be able to
search and replace your variables names.

Why

CSS forge is library that leverages modern CSS features and conventions to help you
generate CSS custom properties (css variables).

At the core of CSSforge is the schema : A serializable configuration object.

CSSforge has 0 runtime and generate at build time raw CSS, Typescript or JSON.

This intentionally keeps things simple and flexible, and allows you to integrate it with
any framework or CSS workflow.

In the future, CSSforge will try to integrate with popular design tools such as Figma.

Features

  • 🎨 Colors: Create palettes, gradients and themes. Automatically convert to OKLCH.
  • 📐 Typography: Generate fluid typography
  • 📏 Spacing: Organise spacing utilities
  • 📦 Primitives: Define custom design tokens
  • 🎯 Zero Runtime: All processing happens at build time
  • 🔄 Watch Mode: Auto-regenerate when your config changes
  • 🔌 Framework Agnostic: Use with any CSS workflow

Installation

For programmatic usage, you can install CSS Forge as a dependency:

# Using npm
npx jsr add @hebilicious/cssforge

# Using pnpm (10.9 +)
pnpm i jsr:@hebilicious/cssforge

Then to run CSS forge, add the following script to your package.json or deno.json :

{
  "scripts": {
    "cssforge": "node node_modules/@hebilicious/cssforge/src/cli"
  }
}

then run :

#npm
npm run cssforge
#pnpm
pnpm run cssforge

Quick Start

  1. Create a configuration file (cssforge.config.ts):
import { defineConfig } from "jsr:@hebilicious/cssforge";

export default defineConfig({
  spacing: {
    custom: {
      size: {
        value: {
          1: "0.25rem",
          2: "0.5rem",
          3: "0.75rem",
          4: "1rem",
        },
      },
    },
  },
  typography: {
    fluid: {
      arial: {
        value: {
          minWidth: 320,
          minFontSize: 14,
          minTypeScale: 1.25,
          maxWidth: 1435,
          maxFontSize: 16,
          maxTypeScale: 1.25,
          positiveSteps: 5,
          negativeSteps: 3,
        },
      },
    },
  },
  colors: {
    palette: {
      value: {
        coral: {
          value: {
            100: { hex: "#FF7F50" },
          },
        },
        mint: {
          value: {
            100: { hex: "#4ADE80" },
          },
        },
        indigo: {
          value: {
            100: { hex: "#4F46E5" },
          },
        },
      },
    },
  },
});
  1. Run CSS Forge with the CLI :

If you’re using a package.json, add the follwing to your scripts :

{
  "scripts": {
    "cssforge": "node node_modules/@hebilicious/cssforge/src/cli"
  }
}

In the future JSR will support using npx @hebilicious/cssforge directly.

Then run the following command :

npm run cssforge # Basic usage
npm run cssforge --help # To see all options
npm run cssforge --watch # To watch for changes

If you’re using a deno.json :

{
  "tasks": {
    "cssforge": "deno run -A jsr:@hebilicious/cssforge/cli"
  }
}

Then :

deno task cssforge # Basic usage
deno task cssforge --help # To see all options
deno task cssforge --watch # To watch for changes
  1. Use the generated variables in your CSS:

For example, you can import as a layer :

@import "cssforge.output.css" layer(cssforge);

.button {
  background-color: var(--color-primary-500);
  padding: var(--size-2) var(--size-4);
}

!IMPORTANT Do not manually edit the generated CSS file, edit the configuration file
instead and regenerate.

  1. Use the generated css in your JS/TS :
import { cssForge } from "./.cssforge/output.ts";

// Use like this anywhere :
//`cssForge.colors.palette.basic.white` is fully typed { key: --myKey, value: white, variable: --key: white }

export { cssForge };

Configuration

Colors

Define colors in any format - they’ll be automatically converted to OKLCH. You can compose
colors from the palette into gradients and themes.

export default defineConfig({
  colors: {
    palette: {
      value: {
        simple: {
          value: {
            white: "oklch(100% 0 0)",
            black: "#000",
            green: { rgb: [0, 255, 0] },
            blue: { hsl: [240, 100, 50] },
            violet: { oklch: "oklch(0.7 0.2 270)" },
            red: { hex: "#FF0000" },
          },
        },
        another: {
          value: {
            yellow: { hex: "#FFFF00" },
            cyan: { hex: "#00FFFF" },
          },
          settings: {
            selector: ".Another",
          },
        },
      },
    },
    gradients: {
      value: {
        "white-green": {
          value: {
            primary: {
              value: "linear-gradient(to right, var(--c1), var(--c2))",
              variables: {
                "c1": "palette.simple.white",
                "c2": "palette.simple.green",
              },
            },
          },
        },
      },
    },
    theme: {
      light: {
        value: {
          background: {
            value: {
              primary: "var(--1)",
              secondary: "var(--2)",
            },
            variables: {
              1: "palette.simple.white",
              2: "gradients.white-green.primary", //Reference the color name directly.
            },
            settings: {
              variantNameOnly: true,
            },
          },
        },
      },
      dark: {
        value: {
          background: {
            value: {
              primary: "var(--1)",
              secondary: "var(--2)",
            },
            variables: {
              1: "palette.another.yellow",
              2: "palette.another.cyan",
            },
            settings: {
              variantNameOnly: true,
            },
          },
        },
        settings: {
          atRule: "@media (prefers-color-scheme: dark)",
        },
      },
      pink: {
        value: {
          background: {
            value: {
              primary: "var(--1)",
              secondary: "var(--2)",
            },
            variables: {
              1: "palette.simple.red",
              2: "palette.simple.violet",
            },
            settings: {
              variantNameOnly: true,
            },
          },
        },
        settings: {
          selector: ".ThemePink",
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
  /*____ Colors ____*/
  /* Palette */
  /* simple */
  --palette-simple-white: oklch(100% 0 0);
  --palette-simple-black: oklch(0% 0 0);
  --palette-simple-green: oklch(86.644% 0.29483 142.49535);
  --palette-simple-blue: oklch(45.201% 0.31321 264.05202);
  --palette-simple-violet: oklch(70% 0.2 270);
  --palette-simple-red: oklch(62.796% 0.25768 29.23388);
  /* Gradients */
  /* white-green */
  --gradients-white-green-primary: linear-gradient(
    to right,
    var(--palette-simple-white),
    var(--palette-simple-green)
  );
  /* Themes */
  /* Theme: light */
  /* background */
  --primary: var(--palette-simple-white);
  --secondary: var(--gradients-white-green-primary);
  /* Theme: dark */
  @media (prefers-color-scheme: dark) {
    /* background */
    --primary: var(--palette-another-yellow);
    --secondary: var(--palette-another-cyan);
  }
}
/* another */
.Another {
  --palette-another-yellow: oklch(96.798% 0.21101 109.76924);
  --palette-another-cyan: oklch(90.54% 0.15455 194.76896);
}
/* Theme: pink */
.ThemePink {
  /* background */
  --primary: var(--palette-simple-red);
  --secondary: var(--palette-simple-violet);
}

Condition

You can conditionnally apply colors, gradients or themes by setting the atRule or the
selector properties. Your variables will be wrapped within :root and the selectors
will be placed outside of it.

Theme: Variant Name Only

When working with themes, you can choose to only include the variant name in the CSS
variable name by setting variantNameOnly: true in the color definition settings. This is
usually used in combination with selector to conditionnally apply themes.

  • Default: --theme-${themeName}-${colorName}-${variantName}
  • VariantOnly Name: --${variantName}
  • Path : theme.${themeName}.${colorName}.${variantName}

Spacing

Define custom spacing scale, that can be referenced for other types, such as primitives.
By default all spacing values are converted to from px to rem. This can be disabled
with the settings.

export default defineConfig({
  spacing: {
    custom: {
      size: {
        value: {
          1: "0.25rem",
          2: "0.5rem",
          3: "0.75rem",
          4: "16px",
        },
        settings: { pxToRem: true, rem: 16 }, // Optional, default settings
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
  /*____ Spacing ____*/
  --spacing-size-1: 0.25rem;
  --spacing-size-2: 0.5rem;
  --spacing-size-3: 0.75rem;
  --spacing-size-4: 1rem;
}

Fluid Spacing (Utopia)

You can generate fluid spacing scales powered by Utopia. Fluid
scales output clamp() expressions which interpolate between a minimum and maximum size
across a viewport range.

export default defineConfig({
  spacing: {
    fluid: {
      base: {
        value: {
          minSize: 4,
          maxSize: 24,
          minWidth: 320,
          maxWidth: 1280,
          negativeSteps: [0],
          positiveSteps: [3],
          prefix: "hi",
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
  /*____ Spacing ____*/
  --spacing_fluid-base-hi-xs: clamp(0rem, 0rem + 0vw, 0rem);
  --spacing_fluid-base-hi-s: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);
  --spacing_fluid-base-hi-m: clamp(0.75rem, -0.5rem + 6.25vw, 4.5rem);
  --spacing_fluid-base-hi-xs-s: clamp(0rem, -0.5rem + 2.5vw, 1.5rem);
  --spacing_fluid-base-hi-s-m: clamp(0.25rem, -1.1667rem + 7.0833vw, 4.5rem);
}

You can combine fluid and static spacing:

export default defineConfig({
  spacing: {
    fluid: {
      base: {
        value: {
          minSize: 4,
          maxSize: 24,
          minWidth: 320,
          maxWidth: 1280,
          positiveSteps: [1.5, 2, 3, 4, 6],
          negativeSteps: [0.75, 0.5, 0.25],
          prefix: "smooth",
        },
      },
    },
    custom: {
      gap: {
        value: { 1: "4px", 2: "8px" },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
  /*____ Spacing ____*/
  --spacing_fluid-base-smooth-3xs: clamp(0.0625rem, -0.0417rem + 0.5208vw, 0.375rem);
  --spacing_fluid-base-smooth-2xs: clamp(0.125rem, -0.0833rem + 1.0417vw, 0.75rem);
  --spacing_fluid-base-smooth-xs: clamp(0.1875rem, -0.125rem + 1.5625vw, 1.125rem);
  --spacing_fluid-base-smooth-s: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);
  --spacing_fluid-base-smooth-m: clamp(0.375rem, -0.25rem + 3.125vw, 2.25rem);
  --spacing_fluid-base-smooth-l: clamp(0.5rem, -0.3333rem + 4.1667vw, 3rem);
  --spacing_fluid-base-smooth-xl: clamp(0.75rem, -0.5rem + 6.25vw, 4.5rem);
  --spacing_fluid-base-smooth-2xl: clamp(1rem, -0.6667rem + 8.3333vw, 6rem);
  --spacing_fluid-base-smooth-3xl: clamp(1.5rem, -1rem + 12.5vw, 9rem);
  --spacing_fluid-base-smooth-3xs-2xs: clamp(0.0625rem, -0.1667rem + 1.1458vw, 0.75rem);
  --spacing_fluid-base-smooth-2xs-xs: clamp(0.125rem, -0.2083rem + 1.6667vw, 1.125rem);
  --spacing_fluid-base-smooth-xs-s: clamp(0.1875rem, -0.25rem + 2.1875vw, 1.5rem);
  --spacing_fluid-base-smooth-s-m: clamp(0.25rem, -0.4167rem + 3.3333vw, 2.25rem);
  --spacing_fluid-base-smooth-m-l: clamp(0.375rem, -0.5rem + 4.375vw, 3rem);
  --spacing_fluid-base-smooth-l-xl: clamp(0.5rem, -0.8333rem + 6.6667vw, 4.5rem);
  --spacing_fluid-base-smooth-xl-2xl: clamp(0.75rem, -1rem + 8.75vw, 6rem);
  --spacing_fluid-base-smooth-2xl-3xl: clamp(1rem, -1.6667rem + 13.3333vw, 9rem);
  --spacing-gap-1: 0.25rem;
  --spacing-gap-2: 0.5rem;
}

Typography

Define your typography, with fluid typescales powered by
utopia:

export default defineConfig({
  typography: {
    weight: {
      arial: {
        value: {
          regular: "600",
        },
      },
    },
    fluid: {
      arial: {
        value: {
          minWidth: 320,
          minFontSize: 14,
          minTypeScale: 1.25,
          maxWidth: 1435,
          maxFontSize: 16,
          maxTypeScale: 1.25,
          positiveSteps: 5,
          negativeSteps: 3,
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
  /*____ Typography ____*/
  --typography_fluid-arial-4xl: clamp(2.6703rem, 2.5608rem + 0.5474vw, 3.0518rem);
  --typography_fluid-arial-3xl: clamp(2.1362rem, 2.0486rem + 0.4379vw, 2.4414rem);
  --typography_fluid-arial-2xl: clamp(1.709rem, 1.6389rem + 0.3503vw, 1.9531rem);
  --typography_fluid-arial-xl: clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem);
  --typography_fluid-arial-l: clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem);
  --typography_fluid-arial-m: clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem);
  --typography_fluid-arial-s: clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem);
  --typography_fluid-arial-xs: clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem);
  --typography_fluid-arial-2xs: clamp(0.448rem, 0.4296rem + 0.0918vw, 0.512rem);
  --typography-weight-arial-regular: 600;
}

Customizing Fluid Typography Scales

You can customize the typescale by providing your prefix and custom labels. The prefix
will overwrite the name of the key that you are using to define your typography.

const config = defineConfig({
  typography: {
    fluid: {
      comicsans: {
        value: {
          minWidth: 320,
          minFontSize: 14,
          minTypeScale: 1.25,
          maxWidth: 1435,
          maxFontSize: 16,
          maxTypeScale: 1.25,
          positiveSteps: 2,
          negativeSteps: 2,
          prefix: "text",
        },
        settings: {
          customLabel: {
            "-2": "a",
            "-1": "b",
            "0": "c",
            "1": "d",
            "2": "e",
          },
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
  /*____ Typography ____*/
  --typography_fluid-comicsans-text-e: clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem);
  --typography_fluid-comicsans-text-d: clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem);
  --typography_fluid-comicsans-text-c: clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem);
  --typography_fluid-comicsans-text-b: clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem);
  --typography_fluid-comicsans-text-a: clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem);
}

Primitives

More flexible than other types, primitives allow you to define any type of token by
composing the base types.

export default defineConfig({
  typography: {
    fluid: {
      arial: {
        value: {
          minWidth: 320,
          minFontSize: 14,
          minTypeScale: 1.25,
          maxWidth: 1435,
          maxFontSize: 16,
          maxTypeScale: 1.25,
          positiveSteps: 5,
          negativeSteps: 3,
        },
      },
    },
  },
  spacing: {
    custom: {
      size: {
        value: {
          2: "0.5rem",
          3: "0.75rem",
        },
      },
    },
  },
  primitives: {
    button: {
      value: {
        small: {
          value: {
            width: "120px",
            height: "40px",
            fontSize: "var(--base)",
            radius: "8px",
            padding: "var(--2) var(--3)",
          },
          variables: {
            "base": "typography_fluid.arial@m",
            "2": "spacing.custom.size.2",
            "3": "spacing.custom.size.3",
          },
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
  /*____ Spacing ____*/
  --spacing-size-2: 0.5rem;
  --spacing-size-3: 0.75rem;
  /*____ Typography ____*/
  --typography_fluid-arial-4xl: clamp(2.6703rem, 2.5608rem + 0.5474vw, 3.0518rem);
  --typography_fluid-arial-3xl: clamp(2.1362rem, 2.0486rem + 0.4379vw, 2.4414rem);
  --typography_fluid-arial-2xl: clamp(1.709rem, 1.6389rem + 0.3503vw, 1.9531rem);
  --typography_fluid-arial-xl: clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem);
  --typography_fluid-arial-l: clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem);
  --typography_fluid-arial-m: clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem);
  --typography_fluid-arial-s: clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem);
  --typography_fluid-arial-xs: clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem);
  --typography_fluid-arial-2xs: clamp(0.448rem, 0.4296rem + 0.0918vw, 0.512rem);
  /*____ Primitives ____*/
  /* button */
  --button-small-width: 7.5rem;
  --button-small-height: 2.5rem;
  --button-small-fontSize: var(--typography_fluid-arial-m);
  --button-small-radius: 0.5rem;
  --button-small-padding: var(--spacing-size-2) var(--spacing-size-3);
}

Referencing Variables

Basic Referencing

To reference any variable, use the . notation to navigate through the schema without
using .value.

For example, if an object has the following structure :

{
  spacing: {
    custom: {
      size: {
        value: {
          1: "0.25rem",
        },
      },
    },
  },
}

The reference would be : spacing.custom.size.1, not spacing.custom.size.value.1.

Referencing Fluid Spacing

To reference fluid spacing, use the @ symbol and the label of the scale; ie:
spacing_fluid-base@xs. Do not include the prefix in the reference. The labels follow the
following convention :

  • 3xs
  • 2xs
  • xs
  • s
  • m
  • l
  • xl
  • 2xl
  • 3xl

Referencing Fluid Typography

To reference fluid typography, use the @ symbol and the label of the scale; ie:
typography_fluid.comicsans@a. Do not include the prefix in the reference. The labels
follow the following convention :

  • 3xs
  • 2xs
  • xs
  • s
  • m
  • l
  • xl
  • 2xl
  • 3xl

CLI Usage

Assuming you have added the script to your package.json, you can run CSS Forge like this
:

# Basic usage
npm run cssforge

# Watch mode
npm run cssforge -- --watch

# Custom paths and output 
npm run cssforge --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --mode all

Programmatic Usage

You can also use CSS Forge programmatically:

import { generateCSS } from "jsr:@hebilicious/cssforge";

// Generate CSS string
const css = generateCSS(config);

Agentic usage

CSSForge is intentionally designed to be extremely simple and integrate well with various
agents, such as Github Copilot, Gemini, Claude Code … While there’s no documentation
yet, you can add the README file to the agent context directly.

For most agents, this syntax works
@https://raw.githubusercontent.com/Hebilicious/cssforge/refs/heads/main/README.md.

Best Practices

  • undefinedVersion Control: Commit your generated CSS files
  • undefinedCSS Layers: Use @layer to manage specificity
  • undefinedConfig First: Always edit the config file, never edit the generated files

Examples

Check out our examples:

TODO

License

MIT

[beta]v0.14.0