@crescendolab/css-var-ts
v1.0.1
Published
Type-safe utilities for CSS Custom Properties
Downloads
14
Readme
🌟 @crescendolab/css-var-ts
Type-safe, ergonomic utilities for authoring, registering, and consuming CSS Custom Properties (CSS Variables) in TypeScript.
🚀 Features
- ✅ Strongly typed CSS variable keys & values
- ✅ Auto–generated collision‑resistant variable names (slug + short random id)
- ✅ Convenient
.cssPropsmap you can spread into inline styles / style objects - ✅ Easy integration with:
@emotion/css,@emotion/react(css prop),@mui/system(sxprop) - ✅ Compose semantic variables from a base palette safely (
getValue→var(--token)) - ✅ Advanced: custom variable key generator via
cssVarUtils.create - ✅ Advanced: works with
@propertyat‑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 loadStatic (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.
Strengthen uniqueness: Provide a custom
recordKeyToCssVarKeythat 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. ofnamespace + key).Strongly recommended: Register core design tokens via
@propertyto enforce syntax (e.g.<color>,<length>) and enable smoother transitions & validation.Expose only semantic tokens to feature teams; keep raw palette tokens private to your design system package.
Document namespace conventions so new packages follow the same pattern.
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
Copyright (c) 2025 Crescendo Lab
Made with ❤️ to make CSS variables first-class citizens in TypeScript.
