npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hebilicious/cssforge

v0.10.1

Published

0 runtime design tokens generator for modern style systems.

Readme

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
  • 🎞️ Motion: Durations and easing curves, checked against UI motion guidelines
  • 📦 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

CSS Forge requires Node 24 or newer. It loads cssforge.config.ts with the native TypeScript support of that runtime, so no extra loader or tsx installation is needed. Configuration files use ES module syntax; add "type": "module" to your package.json to load them without Node's module type detection warning.

# Using npm
npm install --save-dev @hebilicious/cssforge

# Using pnpm
pnpm add -D @hebilicious/cssforge

The package installs a cssforge executable:

pnpm cssforge --mode all # pnpm
npx cssforge --mode all # npm

The rest of this document writes commands as cssforge <args>.

Alternative installation (JSR)

CSS Forge is also published to JSR at the same version as npm. Use it for JSR-native imports:

# npm
npx jsr add @hebilicious/cssforge

# pnpm 10.9+
pnpm i jsr:@hebilicious/cssforge

Both keep the package name, so imports stay import { defineConfig } from "@hebilicious/cssforge";.

Contributing Workflow

This repository is a pnpm workspace orchestrated with moon.

pnpm install
moon run :format
moon run cssforge:test
moon run cssforge:typecheck
moon run cssforge:build
moon run cssforge:smoke-test
moon run cssforge:jsr-smoke
moon run cssforge:jsr-dry-run

cssforge:smoke-test packs the package and installs that tarball in clean npm and pnpm projects, and cssforge:jsr-smoke runs the JSR entry points with Node.

Quick Start

  1. Create a configuration file (cssforge.config.ts):
import { defineConfig } from "@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 :
cssforge # Basic usage
cssforge --help # To see all options
cssforge --watch # To watch for changes
  1. Use the generated variables in your CSS:

The CLI writes ./.cssforge/output.css by default. From a consumer stylesheet placed at the project root, import that file as a layer :

/* Relative to a consumer stylesheet placed at the project root. */
@import "./.cssforge/output.css" layer(cssforge);

.button {
  background-color: var(--palette-coral-100);
  padding: var(--spacing-size-2) var(--spacing-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 :

The CLI also writes ./.cssforge/output.ts, which exports every token as a fully typed cssForge object :

import { cssForge } from "./.cssforge/output.ts";

// Fully typed token : cssForge.spacing.custom.size["2"] is
// { key: "--spacing-size-2", value: "0.5rem", variable: "--spacing-size-2: 0.5rem;" }
export const spacing2 = cssForge.spacing.custom.size["2"];

export { cssForge };

The generated file is a .ts module, so importing it needs "allowImportingTsExtensions": true (with "noEmit": true) in your tsconfig.json.

Bundler Plugin

Generate tokens inside the build instead of running the CLI first. The plugin is powered by unplugin and covers Vite, Rollup, Rolldown, webpack, Rspack, Rsbuild, esbuild, Farm, and Bun.

pnpm add -D @hebilicious/cssforge-unplugin
// vite.config.ts
import cssforge from "@hebilicious/cssforge-unplugin/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [cssforge()],
});
// src/main.ts
import "virtual:cssforge.css";

TypeScript needs the ambient declaration for that import :

// src/vite-env.d.ts
/// <reference types="@hebilicious/cssforge-unplugin/client" />

The stylesheet is generated on demand from cssforge.config.ts. The plugin registers the config and every local module it imports as watch files, so editing a token regenerates it, and a Vite dev server updates the styles without reloading the page.

Each bundler has its own entry point :

| Bundler | Import | | --- | --- | | Vite | @hebilicious/cssforge-unplugin/vite | | Rollup | @hebilicious/cssforge-unplugin/rollup | | Rolldown | @hebilicious/cssforge-unplugin/rolldown | | webpack | @hebilicious/cssforge-unplugin/webpack | | Rspack | @hebilicious/cssforge-unplugin/rspack | | Rsbuild | @hebilicious/cssforge-unplugin/rsbuild | | esbuild | @hebilicious/cssforge-unplugin/esbuild | | Farm | @hebilicious/cssforge-unplugin/farm | | Bun | @hebilicious/cssforge-unplugin/bun |

The plugin options are :

cssforge({
  // Resolved against the build's working directory. Default: ./cssforge.config.ts
  config: "./cssforge.config.ts",
  // Also write the stylesheet to disk. Default: false
  write: { css: "./.cssforge/output.css" },
});

write: true uses ./.cssforge/output.css, the CLI's default path. Use it when another tool needs a real file, or when a bundler has no CSS handling for virtual modules.

The plugin calls the same generateCSS implementation as the CLI, so virtual:cssforge.css matches cssforge --mode css output byte for byte. The served stylesheet is unlayered, and @import "virtual:cssforge.css" layer(cssforge) does not work because a CSS import resolves to a file; set write and import the written file with layer(cssforge) to keep the tokens in a layer. The CLI stays the integration path for everything else: JSR, Style Dictionary, CI steps, and tools without a bundler.

Output Formats

CSS Forge writes more than CSS. One run emits the same tokens as CSS, TypeScript, JSON and Style Dictionary JSON, so values can be imported directly in TypeScript or read from other tools.

| Format | Mode | Default path | Flag | Use it for | | --- | --- | --- | --- | --- | | CSS custom properties | css | ./.cssforge/output.css | --css | Stylesheets, var(--token) | | TypeScript module | ts | ./.cssforge/output.ts | --ts | Importing token values into TS/JS | | JSON | json | ./.cssforge/output.json | --json | Any tool that reads JSON | | Style Dictionary tokens | style-dictionary | ./.cssforge/tokens.sd.json | --style-dictionary | Style Dictionary and compatible tools |

--mode all is the default and writes all four files. Every other mode writes one format, so run the command once per mode. Style Dictionary has its own section, Style Dictionary JSON.

Token objects in TypeScript and JSON

The TypeScript and JSON outputs hold the same nested tree. Every leaf is one token:

{
  "palette": {
    "coral": {
      "100": {
        "key": "--palette-coral-100",
        "value": "oklch(73.511% 0.16799 40.24666)",
        "variable": "--palette-coral-100: oklch(73.511% 0.16799 40.24666);"
      }
    }
  }
}

| Field | Contains | Use it for | | --- | --- | --- | | key | The CSS custom property, such as --palette-coral-100 | Building a var() string, or looking a token up by name | | value | The CSS value, such as oklch(...), 0.5rem, or clamp(...) | Passing a color, a length, or a font size to anything that accepts CSS | | variable | The full declaration, such as --palette-coral-100: oklch(...); | Injecting a declaration into a style tag or a shadow root | | color | The generated formats, such as { "hex": { "string": "#ff7f50" }, "rgb": { "array": [255, 127, 80] } } | Reading a palette color as a legacy value without converting it. Present when formats are configured, on the palette or the color, or added by colorFormats | | gamutMapped | true when the color is outside sRGB | Knowing which colors the browser paints differently from the authored value. Absent inside sRGB, and absent when no format is generated |

A level with one child is collapsed, so palette: { value: { coral: ... } } becomes cssForge.palette.coral. Numeric and @ keys stay strings: cssForge.spacing.custom.size["2"], cssForge.typography_fluid["arial@m"].

Import color strings in TypeScript

The TypeScript output is a typed module. Put it in your source tree:

cssforge --mode ts --ts ./src/design-tokens.ts
import { cssForge } from "./design-tokens.ts";

const coral = cssForge.palette.coral["100"].value; // "oklch(73.511% 0.16799 40.24666)"

// The string is the value: pass it to an SVG fill, a chart series, or a canvas call.
const iconFill = coral;

// The declaration moves a token into a style tag or a shadow root.
const injected = `:root { ${cssForge.spacing.custom.size["2"].variable} }`;

export const card = {
  backgroundColor: coral,
  padding: cssForge.spacing.custom.size["2"].value, // "0.5rem"
  fontSize: cssForge.typography_fluid["arial@m"].value, // "clamp(0.875rem, ...)"
};

export { iconFill, injected };

The module is generated as const, so every path is typed and autocompleted, and a typo fails type checking. Importing it needs "allowImportingTsExtensions": true with "noEmit": true, the compiler options the Quick Start documents.

Read the tokens from other languages

The JSON output is the same tree without the as const wrapper:

cssforge --mode json --json ./src/design-tokens.json
import tokens from "./design-tokens.json" with { type: "json" };

const coral = tokens.palette.coral["100"].value; // "oklch(73.511% 0.16799 40.24666)"

Values are CSS strings

Palette, spacing, typography and motion tokens hold final values, because CSS Forge converts colors to OKLCH and computes fluid scales at build time. Theme, gradient and primitive tokens keep their var(--other-token) reference, because only the CSS cascade knows which value is active:

| Token | value | | --- | --- | | Palette, spacing, typography, motion | The final value, such as oklch(...), 0.5rem, or clamp(...) | | Theme, gradient, primitive | var(--token), resolved by the browser at paint time |

const themed = {
  color: cssForge.theme.light.background.primary.value, // "var(--palette-coral-100)"
  borderColor: cssForge.palette.coral["100"].value, // "oklch(73.511% 0.16799 40.24666)"
};

Both are valid CSS. The first follows the active theme; the second is a snapshot of one theme.

Configuration

Every module holds its tokens under value and its options under settings. A setting no schema accepts is rejected with its configuration path, so a misspelled key cannot quietly generate nothing.

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: ":root.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 none);
--palette-simple-black: oklch(0% 0 none);
--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 */
:root.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);
}

The another palette is emitted under :root.Another, so the element carrying the theme class has to be the root element:

<html class="Another">

Custom property references are substituted when the alias is computed, before inheritance. --primary: var(--palette-another-yellow) is computed on :root, so --palette-another-yellow has to be defined on :root as well. Scoping the palette to :root.Another keeps both declarations on the same element; a theme class on a descendant leaves --primary invalid at computed-value time, and every var(--primary, fallback) reference uses its fallback.

Color formats for browsers without oklch

Palette colors are generated in OKLCH, which a browser without oklch() support cannot render. Set formats to generate sRGB values alongside it, and fallback to declare one of them for those browsers:

export default defineConfig({
  colors: {
    palette: {
      value: {
        coral: { 100: { hex: "#FF7F50" } },
        coralDark: {
          value: { 100: { hex: "#FF6347" } },
          settings: { atRule: "@media (prefers-color-scheme: dark)" },
        },
      },
      settings: {
        color: {
          formats: {
            hex: { string: true, digits: true, number: true },
            rgb: { string: true, array: true },
          },
          fallback: "hex",
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
/*____ Colors ____*/
/* Palette */
/* coral */
--palette-coral-100: oklch(73.511% 0.16799 40.24666);
/* coralDark */
@media (prefers-color-scheme: dark) {
  --palette-coralDark-100: oklch(69.622% 0.19552 32.32143);
}
}
@supports not (color: oklch(0% 0 0)) {
  :root {
    /* coral */
    --palette-coral-100: #ff7f50;
  }
}
@media (prefers-color-scheme: dark) {
  @supports not (color: oklch(0% 0 0)) {
    :root {
      /* coralDark */
      --palette-coralDark-100: #ff6347;
    }
  }
}

| Format | Output | Value | | --- | --- | --- | | hex | string | "#ff7f50" | | hex | digits | "ff7f50" | | hex | number | 16744272 (0xff7f50) | | rgb | string | "rgb(255 127 80)" | | rgb | array | [255, 127, 80] |

A format set to true generates its CSS value. A color with alpha carries it (#ff7f50aa, 0xff7f50aa, rgb(255 127 80 / 0.667), [255, 127, 80, 0.667]) unless the format sets alpha: true keeps it, a number from 0 to 1 sets it, false rejects the color.

The declaration is the string value of fallback, or of the first format that has one, gated by @supports not (color: oklch(0% 0 0)) and mirroring the color's atRule and selector. fallback: false declares nothing. It is emitted with the palette, before gradient and theme blocks, so a later declaration still wins.

A color's formats merge into the palette's per format, and false removes one. Tokens carry every generated value in color, under their format and output:

"color": {
  "hex": { "string": "#ff7f50", "number": 16744272 },
  "rgb": { "array": [255, 127, 80] }
}

A color outside sRGB is gamut mapped for its sRGB values, and its token carries gamutMapped: true so the mapping is visible.

The palette is the only family that converts the colors it is given, so it is the only one that generates formats. Themes and gradients keep their authored values.

Derived colors with mix

Derive hover, subtle and alpha variants from a base color instead of hand-tuning near-duplicates. A mix value is accepted wherever a palette variant or a theme value takes a color, and is emitted as color-mix():

export default defineConfig({
  colors: {
    palette: {
      value: {
        accent: {
          base: "oklch(55% 0.2 264)",
          hover: { mix: { from: "palette.accent.base", with: "black", amount: 15 } },
        },
      },
      settings: { color: { formats: { hex: true } } },
    },
    theme: {
      light: {
        value: {
          action: {
            value: {
              subtle: {
                mix: { from: "palette.accent.base", with: "transparent", amount: 88 },
              },
            },
          },
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
/*____ Colors ____*/
/* Palette */
/* accent */
--palette-accent-base: oklch(55% 0.2 264);
--palette-accent-hover: color-mix(in oklch, var(--palette-accent-base), black 15%);
/* Themes */
/* Theme: light */
/* action */
--theme-light-action-subtle: color-mix(in oklch, var(--palette-accent-base), transparent 88%);
}
@supports not (color: oklch(0% 0 0)) {
  :root {
    /* accent */
    --palette-accent-base: #3266e4;
    --palette-accent-hover: #2651b8;
  }
}
  • from and with are a token path or a CSS color. A dotted path without spaces, parentheses or #, such as "palette.accent.base", is a token path, written like the paths in variables, and is emitted as its var() reference so overriding the base re-derives the variant. It must name a color declared before the mix. Anything else must be a CSS color colorjs.io parses, such as "black", "#fff" or "transparent", and is emitted as written. currentColor and system colors have no static value, so they are rejected.
  • amount is the percentage of with, from 0 to 100. Mixing with transparent produces the alpha variant: amount: 88 keeps the color at 12% opacity.
  • in is the interpolation space. Only "oklch" is accepted, the default.

The color-mix() value reaches the CSS and the tokens; the Style Dictionary resolved value substitutes the referenced colors. When a palette mix generates formats, its sRGB values are computed by mixing the colors in OKLCH the way the browser does, so a browser without oklch() support still gets a color. An achromatic operand such as white has no hue, so it takes the other color's hue instead of drifting. Unknown keys, an amount out of range, an unresolvable path and a value that is not a color are rejected with the configuration path.

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}

Theme: light-dark()

Instead of repeating every theme token under a selector, pair a light and a dark theme with theme.settings.lightDark. Each color is emitted once at :root as light-dark(<light>, <dark>), and the browser picks the value from the element's color-scheme. Theme settings sit beside the themes, so this needs the theme: { value, settings } form. In the short form, where themes are the keys of theme, settings is reserved and cannot name a theme:

export default defineConfig({
  colors: {
    palette: {
      value: {
        neutral: { white: "#ffffff", ink: "#1a1a1a" },
      },
    },
    theme: {
      value: {
        light: {
          value: {
            background: {
              value: { primary: "var(--white)" },
              variables: { white: "palette.neutral.white" },
            },
            text: {
              value: { body: { mix: { from: "palette.neutral.ink", with: "white", amount: 10 } } },
            },
          },
        },
        dark: {
          value: {
            background: {
              value: { primary: "var(--ink)" },
              variables: { ink: "palette.neutral.ink" },
            },
            text: {
              value: { body: { mix: { from: "palette.neutral.white", with: "black", amount: 10 } } },
            },
          },
        },
      },
      settings: {
        lightDark: {
          light: "light",
          dark: "dark",
          colorScheme: { light: '[data-theme="light"]', dark: '[data-theme="dark"]' },
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
/*____ Colors ____*/
/* Palette */
/* neutral */
--palette-neutral-white: oklch(100% 0 none);
--palette-neutral-ink: oklch(21.779% 0 none);
/* Themes */
/* Theme: light-dark(light, dark) */
color-scheme: light dark;
/* background */
--theme-background-primary: light-dark(var(--palette-neutral-white), var(--palette-neutral-ink));
/* text */
--theme-text-body: light-dark(color-mix(in oklch, var(--palette-neutral-ink), white 10%), color-mix(in oklch, var(--palette-neutral-white), black 10%));
}
[data-theme="light"] {
  color-scheme: light;
}
[data-theme="dark"] {
  color-scheme: dark;
}
  • The paired token drops the theme name: --theme-${colorName}-${variantName} at the path theme.${colorName}.${variantName}, or --${variantName} with variantNameOnly. A variantNameOnly reference keeps working unchanged; a path written as theme.light.background.primary becomes theme.background.primary, which every scheme now shares. A mix can reference a paired token declared before it.
  • Both themes must declare the same colors and variants, and a paired color sets variantNameOnly the same way in both. A mismatch is rejected with the missing paths.
  • The paired themes are emitted at :root, so a selector or atRule on either of them is rejected. Other themes keep their own output, and without lightDark nothing changes.
  • :root gets color-scheme: light dark, so the page follows the user's preference. colorScheme is optional: each selector it names gets a rule forcing that scheme, placed after :root, for a theme switcher.
  • The light-dark() value reaches the CSS, the JSON and TypeScript tokens and the Style Dictionary output, whose resolved value substitutes both schemes' colors. The color-scheme declarations reach the CSS only.
  • light-dark() only accepts colors, so pair color values only.
  • Add <meta name="color-scheme" content="light dark"> to the page <head>, so the browser picks the scheme before the CSS loads.
  • light-dark() is supported in Chrome 123, Firefox 120 and Safari 17.5 (Baseline May 2024).

Spacing

Define custom spacing scale, that can be referenced for other types, such as primitives. By default all spacing values are converted from px to rem. This can be disabled with the settings. Each top-level px length is converted, so 4px 8px becomes 0.25rem 0.5rem. Values inside a CSS function such as calc() or var() are left as they are, so write a pill radius as calc(infinity * 1px) rather than a large number like 999px.

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. Every scale is written twice, and every output holds both: each step as one clamp(), and the same steps derived with pow() from the scale's inputs (see Fluid Typography with pow()):

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_fluid-arial-pow-min-width: 20;
--typography_fluid-arial-pow-max-width: 89.6875;
--typography_fluid-arial-pow-min-font-size: 0.875;
--typography_fluid-arial-pow-max-font-size: 1;
--typography_fluid-arial-pow-min-type-scale: 1.25;
--typography_fluid-arial-pow-max-type-scale: 1.25;
--typography_fluid-arial-pow-progress: clamp(0rem, (100vw - var(--typography_fluid-arial-pow-min-width) * 1rem) / (var(--typography_fluid-arial-pow-max-width) - var(--typography_fluid-arial-pow-min-width)), 1rem);
--typography_fluid-arial-pow-at-min: calc(var(--typography_fluid-arial-pow-min-font-size) * (1rem - var(--typography_fluid-arial-pow-progress)));
--typography_fluid-arial-pow-at-max: calc(var(--typography_fluid-arial-pow-max-font-size) * var(--typography_fluid-arial-pow-progress));
--typography_fluid-arial-pow-4xl: calc(var(--typography_fluid-arial-pow-at-min) * pow(var(--typography_fluid-arial-pow-min-type-scale), 5) + var(--typography_fluid-arial-pow-at-max) * pow(var(--typography_fluid-arial-pow-max-type-scale), 5));
--typography_fluid-arial-pow-3xl: calc(var(--typography_fluid-arial-pow-at-min) * pow(var(--typography_fluid-arial-pow-min-type-scale), 4) + var(--typography_fluid-arial-pow-at-max) * pow(var(--typography_fluid-arial-pow-max-type-scale), 4));
--typography_fluid-arial-pow-2xl: calc(var(--typography_fluid-arial-pow-at-min) * pow(var(--typography_fluid-arial-pow-min-type-scale), 3) + var(--typography_fluid-arial-pow-at-max) * pow(var(--typography_fluid-arial-pow-max-type-scale), 3));
--typography_fluid-arial-pow-xl: calc(var(--typography_fluid-arial-pow-at-min) * pow(var(--typography_fluid-arial-pow-min-type-scale), 2) + var(--typography_fluid-arial-pow-at-max) * pow(var(--typography_fluid-arial-pow-max-type-scale), 2));
--typography_fluid-arial-pow-l: calc(var(--typography_fluid-arial-pow-at-min) * var(--typography_fluid-arial-pow-min-type-scale) + var(--typography_fluid-arial-pow-at-max) * var(--typography_fluid-arial-pow-max-type-scale));
--typography_fluid-arial-pow-m: calc(var(--typography_fluid-arial-pow-at-min) + var(--typography_fluid-arial-pow-at-max));
--typography_fluid-arial-pow-s: calc(var(--typography_fluid-arial-pow-m) / var(--typography_fluid-arial-pow-min-type-scale));
--typography_fluid-arial-pow-xs: calc(var(--typography_fluid-arial-pow-m) / pow(var(--typography_fluid-arial-pow-min-type-scale), 2));
--typography_fluid-arial-pow-2xs: calc(var(--typography_fluid-arial-pow-m) / pow(var(--typography_fluid-arial-pow-min-type-scale), 3));
--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);
--typography_fluid-comicsans-text-pow-min-width: 20;
--typography_fluid-comicsans-text-pow-max-width: 89.6875;
--typography_fluid-comicsans-text-pow-min-font-size: 0.875;
--typography_fluid-comicsans-text-pow-max-font-size: 1;
--typography_fluid-comicsans-text-pow-min-type-scale: 1.25;
--typography_fluid-comicsans-text-pow-max-type-scale: 1.25;
--typography_fluid-comicsans-text-pow-progress: clamp(0rem, (100vw - var(--typography_fluid-comicsans-text-pow-min-width) * 1rem) / (var(--typography_fluid-comicsans-text-pow-max-width) - var(--typography_fluid-comicsans-text-pow-min-width)), 1rem);
--typography_fluid-comicsans-text-pow-at-min: calc(var(--typography_fluid-comicsans-text-pow-min-font-size) * (1rem - var(--typography_fluid-comicsans-text-pow-progress)));
--typography_fluid-comicsans-text-pow-at-max: calc(var(--typography_fluid-comicsans-text-pow-max-font-size) * var(--typography_fluid-comicsans-text-pow-progress));
--typography_fluid-comicsans-text-pow-e: calc(var(--typography_fluid-comicsans-text-pow-at-min) * pow(var(--typography_fluid-comicsans-text-pow-min-type-scale), 2) + var(--typography_fluid-comicsans-text-pow-at-max) * pow(var(--typography_fluid-comicsans-text-pow-max-type-scale), 2));
--typography_fluid-comicsans-text-pow-d: calc(var(--typography_fluid-comicsans-text-pow-at-min) * var(--typography_fluid-comicsans-text-pow-min-type-scale) + var(--typography_fluid-comicsans-text-pow-at-max) * var(--typography_fluid-comicsans-text-pow-max-type-scale));
--typography_fluid-comicsans-text-pow-c: calc(var(--typography_fluid-comicsans-text-pow-at-min) + var(--typography_fluid-comicsans-text-pow-at-max));
--typography_fluid-comicsans-text-pow-b: calc(var(--typography_fluid-comicsans-text-pow-c) / var(--typography_fluid-comicsans-text-pow-min-type-scale));
--typography_fluid-comicsans-text-pow-a: calc(var(--typography_fluid-comicsans-text-pow-c) / pow(var(--typography_fluid-comicsans-text-pow-min-type-scale), 2));
}

Fluid Typography Checks

Each fluid step is checked from its smallest and largest size in px at minWidth and maxWidth. Step 0 and the steps above it take utopia's sizes. A step below 0 is step 0 divided by minTypeScale once per step at every width, so a small size never shrinks as the screen grows. Both representations of a scale use these sizes, so each step is checked once and the checks hold for both:

  • Error: more than 2.5× growth. A step whose maximum is more than 2.5 times its minimum fails the build. Past that ratio, 500% browser zoom cannot double the text at some viewport widths (WCAG 1.4.4 Resize Text). Exactly 2.5× passes. Lower maxFontSize or maxTypeScale, or reduce positiveSteps.
  • Warning: a static scale. A scale whose every step changes by less than 10% across the viewport range gets one warning, because its steps are effectively static. A single static step is normal where a scale crosses over, usually near the base step, so it is not reported on its own.
  • Warning: below the legibility floor. A step whose smaller size is under settings.minLegibleSize (default 12, in px) gets one warning. The first example above warns for s (11.2px), xs (8.96px) and 2xs (7.17px).

When maxFontSize is smaller than minFontSize, the steps shrink as the viewport grows. Those steps are checked at their smaller end, and the 2.5× error only applies to steps that grow.

settings.minLegibleSize takes a number of px greater than 0, or false to turn the floor off. It only changes the warnings: the tokens and the CSS stay the same.

export default defineConfig({
  typography: {
    fluid: {
      caption: {
        value: {
          minWidth: 320,
          minFontSize: 11,
          minTypeScale: 1.2,
          maxWidth: 1280,
          maxFontSize: 13,
          maxTypeScale: 1.25,
          positiveSteps: 1,
          negativeSteps: 0,
        },
        settings: {
          minLegibleSize: 10,
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
/*____ Typography ____*/
--typography_fluid-caption-l: clamp(0.825rem, 0.7615rem + 0.3177vw, 1.0156rem);
--typography_fluid-caption-m: clamp(0.6875rem, 0.6458rem + 0.2083vw, 0.8125rem);
--typography_fluid-caption-pow-min-width: 20;
--typography_fluid-caption-pow-max-width: 80;
--typography_fluid-caption-pow-min-font-size: 0.6875;
--typography_fluid-caption-pow-max-font-size: 0.8125;
--typography_fluid-caption-pow-min-type-scale: 1.2;
--typography_fluid-caption-pow-max-type-scale: 1.25;
--typography_fluid-caption-pow-progress: clamp(0rem, (100vw - var(--typography_fluid-caption-pow-min-width) * 1rem) / (var(--typography_fluid-caption-pow-max-width) - var(--typography_fluid-caption-pow-min-width)), 1rem);
--typography_fluid-caption-pow-at-min: calc(var(--typography_fluid-caption-pow-min-font-size) * (1rem - var(--typography_fluid-caption-pow-progress)));
--typography_fluid-caption-pow-at-max: calc(var(--typography_fluid-caption-pow-max-font-size) * var(--typography_fluid-caption-pow-progress));
--typography_fluid-caption-pow-l: calc(var(--typography_fluid-caption-pow-at-min) * var(--typography_fluid-caption-pow-min-type-scale) + var(--typography_fluid-caption-pow-at-max) * var(--typography_fluid-caption-pow-max-type-scale));
--typography_fluid-caption-pow-m: calc(var(--typography_fluid-caption-pow-at-min) + var(--typography_fluid-caption-pow-at-max));
}

Warnings never fail the build. The CLI prints each one to stderr as cssforge: warning: <message> and still writes the outputs, and getDiagnostics(config) returns them to programmatic callers (see Programmatic Usage).

Fluid Typography with pow()

Every fluid scale is written twice from the same sizes, and both representations are ordinary tokens: the CSS, JSON, TypeScript and Style Dictionary outputs all hold them, and a primitive can reference either.

  • clamp(): --typography_fluid-<scale>[-<prefix>]-<label>, referenced as typography_fluid.<scale>@<label>. Each step is one self-contained clamp(). Use it where a value has to work on its own, such as a token copied into another tool, or a browser without pow().
  • pow(): --typography_fluid-<scale>[-<prefix>]-pow-<label>, referenced as typography_fluid.<scale>.pow@<label>. Each step is derived from the scale's inputs with pow(), so changing an input on :root tunes the whole scale at runtime, in DevTools or from a theme. Change an input in the rule that declares the steps: a step takes its value where it is declared, so an input set on a descendant changes nothing below it. It needs pow(): Chrome 120, Firefox 118, Safari 15.4.

Both give every step the same size at minWidth and maxWidth. Step 0 and the steps above it are utopia's, and their clamp() is the one utopia writes. A step below 0 is step 0 divided by minTypeScale^n at every width: it grows with step 0, and its largest size is maxFontSize / minTypeScale^n rather than utopia's maxFontSize / maxTypeScale^n, so a small size never shrinks as the screen grows.

The pow inputs and helpers of each scale are named --typography_fluid-<scale>[-<prefix>]-pow-<name> and referenced as typography_fluid.<scale>.pow.<name>:

  • min-width and max-width: minWidth and maxWidth in rem, as plain numbers. A length divided by a length does not work in Firefox, so the inputs carry no unit.
  • min-font-size and max-font-size: minFontSize and maxFontSize in rem.
  • min-type-scale and max-type-scale: minTypeScale and maxTypeScale.
  • progress, at-min and at-max: the helpers the steps are built from.

Step 0 is at-min + at-max, step n above it is at-min × pow(min-type-scale, n) + at-max × pow(max-type-scale, n), and step -n is pow step 0 divided by pow(min-type-scale, n). The inputs are primitive tokens. The helpers and steps reference other tokens, so they are semantic and list those tokens as their reference paths.

A step label that gives a pow name, such as customLabel min-width (pow step --typography_fluid-<scale>-pow-min-width) or pow-min-width, is a key collision and fails the build. A step cannot be labelled pow, the segment that holds the pow tokens.

relativeTo sets the width the scale follows: "viewport-width" (the default) writes vw, "viewport" writes vi, and "container" writes cqi for a scale that follows its container. The clamp() steps use it in their slope, and the pow tokens in progress.

export default defineConfig({
  typography: {
    fluid: {
      body: {
        value: {
          minWidth: 320,
          minFontSize: 18,
          minTypeScale: 1.2,
          maxWidth: 1240,
          maxFontSize: 20,
          maxTypeScale: 1.25,
          positiveSteps: 2,
          negativeSteps: 1,
          relativeTo: "container",
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
/*____ Typography ____*/
--typography_fluid-body-xl: clamp(1.62rem, 1.5041rem + 0.5793cqi, 1.9531rem);
--typography_fluid-body-l: clamp(1.35rem, 1.2761rem + 0.3696cqi, 1.5625rem);
--typography_fluid-body-m: clamp(1.125rem, 1.0815rem + 0.2174cqi, 1.25rem);
--typography_fluid-body-s: clamp(0.9375rem, 0.9013rem + 0.1812cqi, 1.0417rem);
--typography_fluid-body-pow-min-width: 20;
--typography_fluid-body-pow-max-width: 77.5;
--typography_fluid-body-pow-min-font-size: 1.125;
--typography_fluid-body-pow-max-font-size: 1.25;
--typography_fluid-body-pow-min-type-scale: 1.2;
--typography_fluid-body-pow-max-type-scale: 1.25;
--typography_fluid-body-pow-progress: clamp(0rem, (100cqi - var(--typography_fluid-body-pow-min-width) * 1rem) / (var(--typography_fluid-body-pow-max-width) - var(--typography_fluid-body-pow-min-width)), 1rem);
--typography_fluid-body-pow-at-min: calc(var(--typography_fluid-body-pow-min-font-size) * (1rem - var(--typography_fluid-body-pow-progress)));
--typography_fluid-body-pow-at-max: calc(var(--typography_fluid-body-pow-max-font-size) * var(--typography_fluid-body-pow-progress));
--typography_fluid-body-pow-xl: calc(var(--typography_fluid-body-pow-at-min) * pow(var(--typography_fluid-body-pow-min-type-scale), 2) + var(--typography_fluid-body-pow-at-max) * pow(var(--typography_fluid-body-pow-max-type-scale), 2));
--typography_fluid-body-pow-l: calc(var(--typography_fluid-body-pow-at-min) * var(--typography_fluid-body-pow-min-type-scale) + var(--typography_fluid-body-pow-at-max) * var(--typography_fluid-body-pow-max-type-scale));
--typography_fluid-body-pow-m: calc(var(--typography_fluid-body-pow-at-min) + var(--typography_fluid-body-pow-at-max));
--typography_fluid-body-pow-s: calc(var(--typography_fluid-body-pow-m) / var(--typography_fluid-body-pow-min-type-scale));
}

Motion

Define transition durations and easing curves once, then reference them from primitives. Durations are grouped under motion.duration and easings under motion.easing. Each group holds its tokens under value, emitted as --motion-duration-<group>-<name> and --motion-easing-<group>-<name>, and referenced as motion.duration.<group>.<name> and motion.easing.<group>.<name>.

  • A duration is a non-negative number with ms or s, such as 120ms or 0.2s. CSS needs a unit on a time, so write 0ms rather than 0.
  • An easing is linear, ease, ease-in, ease-out, ease-in-out, step-start, step-end, cubic-bezier(x1, y1, x2, y2) with x1 and x2 between 0 and 1, steps() or linear().

Anything else fails the build with the token's configuration path.

Two curves work well for UI motion: an ease-out, cubic-bezier(0.23, 1, 0.32, 1), for things entering or reacting, and an ease-in-out, cubic-bezier(0.77, 0, 0.175, 1), for things moving on screen.

export default defineConfig({
  motion: {
    duration: {
      ui: { value: { press: "120ms", tooltip: "150ms", dropdown: "200ms" } },
      overlay: { value: { drawer: "400ms" }, settings: { long: true } },
    },
    easing: {
      ui: {
        value: {
          out: "cubic-bezier(0.23, 1, 0.32, 1)",
          inOut: "cubic-bezier(0.77, 0, 0.175, 1)",
        },
      },
    },
  },
});

This will generate the following CSS :

/*____ CSSForge ____*/
:root {
/*____ Motion ____*/
--motion-duration-ui-press: 120ms;
--motion-duration-ui-tooltip: 150ms;
--motion-duration-ui-dropdown: 200ms;
--motion-duration-overlay-drawer: 400ms;
--motion-easing-ui-out: cubic-bezier(0.23, 1, 0.32, 1);
--motion-easing-ui-inOut: cubic-bezier(0.77, 0, 0.175, 1);
}

Motion Checks

These checks only report warnings: the tokens and the CSS stay the same.

  • Warning: a duration over 300ms (motion-long-duration). A UI transition should take 300ms or less. A modal or drawer may take longer: set settings.long: true on its duration group to raise the limit to 500ms. Exactly 300ms, or 500ms in a long group, passes.
  • Warning: an ease-in curve (motion-ease-in). A curve that starts slow and ends fast reads as lag on UI. ease-in warns, and so does a cubic-bezier() whose slope at the start is below 1 and whose slope at the end is above 1. Each slope is taken toward the nearest control point that does not sit on that end: y1 / x1 at the start and (1 - y2) / (1 - x2) at the end. ease, ease-out, ease-in-out and linear do not warn, and neither do steps() and linear().

settings.long is the only motion setting. It is read on duration groups, and an easing group rejects any setting. CSS Forge only emits the tokens: whether something animates, and the prefers-reduced-motion: no-preference query around the motion, stay in your stylesheet.

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);
--typography_fluid-arial-pow-min-width: 20;
--typography_fluid-arial-pow-max-width: 89.6875;
--typography_fluid-arial-pow-min-font-size: 0.875;
--typography_fluid-arial-pow-max-font-size: 1;
--typography_fluid-arial-pow-min-type-scale: 1.25;
--typography_fluid-arial-pow-max-type-scale: 1.25;
--typography_fluid-arial-pow-progress: clamp(0rem, (100vw - var(--typography_fluid-arial-pow-min-width) * 1rem) / (var(--typography_fluid-arial-pow-max-width) - var(--typography_fluid-arial-pow-min-width)), 1rem);
--typography_fluid-arial-pow-at-min: calc(var(--typography_fluid-arial-pow-min-font-size) * (1rem - var(--typography_fluid-arial-pow-progress)));
--typography_fluid-arial-pow-at-max: calc(var(--typography_fluid-arial-pow-max-font-size) * var(--typography_fluid-arial-pow-progress));
--typography_fluid-arial-pow-4xl: calc(var(--typography_fluid-arial-pow-at-min) * pow(var(--typography_fluid-arial-pow-min-type-scale), 5) + var(--typography_fluid-arial-pow-at-max) * pow(var(--typography_fluid-arial-pow-max-type-scale), 5));
--typography_fluid-arial-pow-3xl: calc(var(--typography_fluid-arial-pow-at-min) * pow(var(--typography_fluid-arial-pow-min-type-scale), 4) + var(--typography_fluid-arial-pow-at-max) * pow(var(--typography_fluid-arial-pow-max-type-scale), 4));
--typography_fluid-arial-pow-2xl: calc(var(--typography_fluid-arial-pow-at-min) * pow(var(--typography_fluid-arial-pow-min-type-scale), 3) + var(--typography_fluid-arial-pow-at-max) * pow(var(--typography_fluid-arial-pow-max-type-scale), 3));
--typography_fluid-arial-pow-xl: calc(var(--typography_fluid-arial-pow-at-min) * pow(var(--typography_fluid-arial-pow-min-type-scale), 2) + var(--typography_fluid-arial-pow-at-max) * pow(var(--typography_fluid-arial-pow-max-type-scale), 2));
--typography_fluid-arial-pow-l: calc(var(--typography_fluid-arial-pow-at-min) * var(--typography_fluid-arial-pow-min-type-scale) + var(--typography_fluid-arial-pow-at-max) * var(--typography_fluid-arial-pow-max-type-scale));
--typography_fluid-arial-pow-m: calc(var(--typography_fluid-arial-pow-at-min) + var(--typography_fluid-arial-pow-at-max));
--typography_fluid-arial-pow-s: calc(var(--typography_fluid-arial-pow-m) / var(--typography_fluid-arial-pow-min-type-scale));
--typography_fluid-arial-pow-xs: calc(var(--typography_fluid-arial-pow-m) / pow(var(--typography_fluid-arial-pow-min-type-scale), 2));
--typography_fluid-arial-pow-2xs: calc(var(--typography_fluid-arial-pow-m) / pow(var(--typography_fluid-arial-pow-min-type-scale), 3));
/*____ 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. Motion tokens follow the same rule: motion.duration.ui.press and motion.easing.ui.out.

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

The pow representation of a step adds a pow segment, as in typography_fluid.comicsans.pow@a, and its inputs and helpers are typography_fluid.comicsans.pow.<name>, such as typography_fluid.comicsans.pow.max-type-scale.