Skip to content

crescendolab-open/css-var-ts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🌟 @crescendolab/css-var-ts

Type-safe, ergonomic utilities for authoring, registering, and consuming CSS Custom Properties (CSS Variables) in TypeScript.

npm (scoped)


🚀 Features

  • âś… Strongly typed CSS variable keys & values
  • âś… Auto–generated collision‑resistant variable names (slug + short random id)
  • âś… Convenient .cssProps map you can spread into inline styles / style objects
  • âś… Easy integration with: @emotion/css, @emotion/react (css prop), @mui/system (sx prop)
  • âś… Compose semantic variables from a base palette safely (getValue → var(--token))
  • âś… Advanced: custom variable key generator via cssVarUtils.create
  • âś… Advanced: works with @property at‑rule registration

📦 Installation

pnpm add @crescendolab/css-var-ts
# or
npm i @crescendolab/css-var-ts
# or
yarn add @crescendolab/css-var-ts

⚡ Quick Start

import { cssVarUtils } from "@crescendolab/css-var-ts";

// 1. Define a base palette
const palette = cssVarUtils.define({
  primaryBlue: "#0074D9",
  accentPink: "#F012BE",
  neutralBg: "#FFFFFF",
  neutralFg: "#111111",
});

// 2. Define semantic tokens referencing the palette (type‑safe)
const semantic = cssVarUtils.define({
  brand: palette.getValue("primaryBlue"),
  text: palette.getValue("neutralFg"),
  background: palette.getValue("neutralBg"),
});

// 3. Use in styles
const style: React.CSSProperties = {
  // ...palette.cssProps, // Optional: variables have fallback values via `getValue`
  // ...semantic.cssProps,
  color: semantic.getValue("text"),
  backgroundColor: semantic.getValue("background"),
};

Resulting (example) generated variable keys (random 8‑char suffix) look like:

--primaryblue-a1b2c3d4
--accentpink-9fe012ab

🧩 Basic Usage (from Storybook “01_basic”)

import { cssVarUtils } from "@crescendolab/css-var-ts";

// Base palette
const paletteDefinition = cssVarUtils.define({
  navy: "#001F3F",
  blue: "#0074D9",
  aqua: "#7FDBFF",
  black: "#111111",
});

// Semantic tokens referencing base palette
const semanticDefinition = cssVarUtils.define({
  primary: paletteDefinition.getValue("navy"),
  foreground: paletteDefinition.getValue("black"),
});

// Override one semantic var dynamically
const dynamicStyle = {
  ...paletteDefinition.cssProps,
  ...semanticDefinition.cssProps,
  [semanticDefinition.getKey("primary")]: paletteDefinition.getValue("blue"),
  color: semanticDefinition.getValue("foreground"),
};

🎨 Integrations

Emotion (@emotion/css)

import { css } from "@emotion/css";
import {
  gruvboxCssVarBaseDefinition,
  gruvboxCssVarLightDefinition,
} from "./styles";

const container = css({
  ...gruvboxCssVarBaseDefinition.cssProps,
  ...gruvboxCssVarLightDefinition.cssProps,
  color: gruvboxCssVarLightDefinition.getValue("fg"),
});

Emotion (css prop)

import { css } from "@emotion/react";
const button = css({
  color: gruvboxCssVarLightDefinition.getValue("fg"),
  backgroundColor: gruvboxCssVarLightDefinition.getValue("bg"),
});

MUI (sx prop)

<Box
  sx={{
    ...gruvboxCssVarBaseDefinition.cssProps,
    ...gruvboxCssVarLightDefinition.cssProps,
    color: gruvboxCssVarLightDefinition.getValue("fg"),
  }}
/>

See live Storybook demos below for full examples including light/dark variants and status colors.


🛠️ Advanced

Custom Variable Key Strategy

Use createCssVarUtils to fully control how variable names are produced (e.g. ephemeral / randomized keys).

import { cssVarUtils, randomString, slugify } from "@crescendolab/css-var-ts";

const myCssVarUtils = cssVarUtils.create({
  recordKeyToCssVarKey: (key) =>
    `--my-${slugify(key)}-${randomString(8)}` as const,
});

const myDefinition = myCssVarUtils.define({
  primary: "#0074D9",
});

myDefinition.getKey("primary"); // different each load

Static (Deterministic) Keys

If you prefer fully readable, deterministic variable names (no random suffix) you can supply a static strategy. Be sure to manually ensure uniqueness across packages / bundles when using this approach.

import { cssVarUtils, slugify } from "@crescendolab/css-var-ts";

const staticCssVarUtils = cssVarUtils.create({
  recordKeyToCssVarKey: (key) => `--static-${slugify(key)}` as const,
});

const staticDefinition = staticCssVarUtils.define({
  primary: "#0074D9",
  accent: "#F012BE",
});

staticDefinition.getKey("primary"); // "--static-primary"
staticDefinition.getValue("primary"); // "var(--static-primary, #0074D9)"

@property Registration

You can register variables with the CSS Typed OM for transitions, inheritance, etc.

const definition = cssVarUtils.define({ primaryColor: "#F012BE" });

CSS.registerProperty({
  name: definition.getKey("primaryColor"),
  syntax: "<color>",
  inherits: true,
  initialValue: "#F012BE",
});

Recommendations for Large CSS-in-JS Apps

For large-scale web applications (mono-repos, micro frontends, dynamic plugin architectures) you should take extra precautions to avoid accidental variable name collisions and to harden your design system surface.

  1. Strengthen uniqueness: Provide a custom recordKeyToCssVarKey that injects a namespace (package name) plus a short random suffix. (You can optionally add build / commit info if desired.)

    import {
      cssVarUtils,
      randomString,
      slugify,
    } from "@crescendolab/css-var-ts";
    
    const namespace = process.env.APP_NAMESPACE ?? "app"; // e.g. marketing, analytics
    
    const scopedCssVarUtils = cssVarUtils.create({
      recordKeyToCssVarKey: (key) =>
        `--${namespace}-${slugify(key)}-${randomString(8)}` as const,
    });

    For deterministic builds replace randomString(8) with a stable hash (e.g. of namespace + key).

  2. Strongly recommended: Register core design tokens via @property to enforce syntax (e.g. <color>, <length>) and enable smoother transitions & validation.

  3. Expose only semantic tokens to feature teams; keep raw palette tokens private to your design system package.

  4. Document namespace conventions so new packages follow the same pattern.

  5. Periodically audit generated variable names (e.g. collect with a build script) to detect drift or duplication.

These measures reduce the chance of silent styling regressions when independently deployed bundles are combined at runtime.


🔍 API Reference

cssVarUtils

The default exported utility bundle.

const definition = cssVarUtils.define({ accent: "#F012BE" });
definition.raw; // [{ accent: "#F012BE" }]
// example suffix will differ each run (8 random hex chars):
definition.cssProps; // { "--accent-a1b2c3d4": "#F012BE" }
definition.getKey("accent"); // "--accent-a1b2c3d4"
definition.getValue("accent"); // "var(--accent-a1b2c3d4, #F012BE)"

Each call to define() returns an object:

Key Type Description
raw [base, ...exts] Array of raw token records (base + extensions)
cssProps Record<cssVarKey, string> Object you can spread into style systems to declare variables
getKey(name) string Generated CSS variable name (e.g. --accent-…)
getValue(name) var(--token, val) Proper var() usage string

cssVarUtils.create(options)

Low‑level factory to customize naming.

const custom = cssVarUtils.create({
  recordKeyToCssVarKey: (k) => `--my-${k}` as const,
});

Helper Exports

Export Purpose
slugify Deterministic slug for record keys
randomString Cryptographically strong random id (hex) for custom strategies

📚 Storybook Examples

Category Story Code Live Demo
Basic: Simple Palette only 01_basic/01_simple Demo
Basic: Extend .extend() usage 01_basic/02_extend Demo
Basic: Reset Nested reset 01_basic/03_reset Demo
Emotion (class) @emotion/css 02_integration/01_emotion/01_emotion_css Demo
Emotion (css prop) @emotion/react 02_integration/01_emotion/02_css_prop Demo
MUI sx prop 02_integration/02_mui_sx_prop Demo
Advanced Static custom keys 03_advanced/01_staticCssVarKey Demo
Advanced @property 03_advanced/02_@property_atRule Demo

🤔 Why add a random suffix?

Adding a short random suffix mitigates accidental collisions when multiple packages / microfrontends define the same token names. It keeps names mostly human readable while providing lightweight namespacing. For fully deterministic readable names use a static strategy; for strict isolation include a package or build id.

Strategy Summary

List of approaches:

  • Default (cssVarUtils): Slug + random 8‑char id = collision‑resistant and readable.
  • Static custom (see story): --static-${slug} for fully readable tokens; ensure uniqueness manually.
  • Random / ephemeral: cssVarUtils.create + randomString / build hash for experiments, multi‑tenant isolation, A/B variants.

đź§Ş Testing Strategy

Library surface is pure & easily unit testable (see randomString.test.ts for an example). Add tests as you add helpers: focus on stability of generated keys and referential integrity between getKey and getValue.


đź›  Release Automation

This repo uses changesets + GitHub Actions. On merge to main, a version PR is created / updated. Approve & merge to publish.

Ensure org settings allow the workflow to create & approve PRs: Settings → Code and automation → Actions → General → Workflow permissions:

  • Read & write permissions
  • Allow GitHub Actions to create and approve pull requests

🤝 Contributing

PRs welcome! See the contributing guide.

Suggested areas:

  • New integrations (e.g. Tailwind plugin example)
  • Additional DX helpers
  • Documentation improvements

📜 License

Apache-2.0

Copyright (c) 2025 Crescendo Lab


Made with ❤️ to make CSS variables first-class citizens in TypeScript.