https://github.com/crescendolab-open/css-var-ts
Type-safe utilities for CSS Custom Properties
https://github.com/crescendolab-open/css-var-ts
css-in-js css-properties css-variables design-system emotion emotionjs figma mui mui-joy mui-material mui-system reactjs storybook type-safe typescript variables
Last synced: 7 months ago
JSON representation
Type-safe utilities for CSS Custom Properties
- Host: GitHub
- URL: https://github.com/crescendolab-open/css-var-ts
- Owner: crescendolab-open
- License: apache-2.0
- Created: 2025-10-07T05:45:03.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2025-10-08T12:36:06.000Z (7 months ago)
- Last Synced: 2025-10-08T14:25:49.690Z (7 months ago)
- Topics: css-in-js, css-properties, css-variables, design-system, emotion, emotionjs, figma, mui, mui-joy, mui-material, mui-system, reactjs, storybook, type-safe, typescript, variables
- Language: TypeScript
- Homepage: https://crescendolab-open.github.io/css-var-ts/
- Size: 1.73 MB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# 🌟 @crescendolab/css-var-ts
Type-safe, ergonomic utilities for authoring, registering, and consuming CSS Custom Properties (CSS Variables) in TypeScript.
[](https://www.npmjs.com/package/@crescendolab/css-var-ts)
---
## 🚀 Features
- âś… Strongly typed CSS variable keys & values
- ✅ Auto–generated collision‑resistant variable names (slug + short random id)
- âś… Zero dependency (createCssVarUtils)
- âś… 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 `createCssVarUtils`
- ✅ Advanced: works with `@property` at‑rule registration
---
## 📦 Installation
```bash
pnpm add @crescendolab/css-var-ts
# or
npm i @crescendolab/css-var-ts
# or
yarn add @crescendolab/css-var-ts
```
---
## ⚡ Quick Start
```ts
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,
...semantic.cssProps,
color: semantic.getValue("text"),
backgroundColor: semantic.getValue("background"),
};
```
Resulting (example) generated variable keys (random 8‑char suffix) look like:
```text
--primaryblue-a1b2c3d4
--accentpink-9fe012ab
```
---
## 🧩 Basic Usage (from Storybook “01_basic”)
```ts
import { cssVarUtils } from "@crescendolab/css-var-ts";
const paletteDefinition = cssVarUtils.define({
navy: "#001F3F",
blue: "#0074D9",
aqua: "#7FDBFF",
// ...
});
const semantic = cssVarUtils.define({
primary: paletteDefinition.getValue("navy"),
foreground: paletteDefinition.getValue("black"),
});
// Override one semantic var dynamically
const dynamicStyle = {
...paletteDefinition.cssProps,
...semantic.cssProps,
[semantic.getKey("primary")]: paletteDefinition.getValue("blue"),
color: semantic.getValue("foreground"),
};
```
Why the two steps? You keep a raw color inventory (can later switch based on theme) and build semantic tokens referencing it. Both sets remain type‑safe.
---
## 🎨 Integrations
### Emotion (`@emotion/css`)
```ts
import { css } from "@emotion/css";
import {
gruvboxCssVarBaseDefinition,
gruvboxCssVarLightDefinition,
} from "./styles";
const container = css({
...gruvboxCssVarBaseDefinition.cssProps,
...gruvboxCssVarLightDefinition.cssProps,
color: gruvboxCssVarLightDefinition.getValue("fg"),
});
```
### Emotion (`css` prop)
```tsx
import { css } from "@emotion/react";
const button = css({
color: gruvboxCssVarLightDefinition.getValue("fg"),
backgroundColor: gruvboxCssVarLightDefinition.getValue("bg"),
});
```
### MUI (`sx` prop)
```tsx
```
> 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).
```ts
import { createCssVarUtils } from "@crescendolab/css-var-ts";
const randomCssVarUtils = createCssVarUtils({
recordKeyToCssVarKey: () =>
`--random-${Math.random().toString(16).slice(2)}` as const,
});
const randomVars = randomCssVarUtils.define({
primary: "#0074D9",
});
randomVars.getKey("primary"); // different each load
```
### `@property` Registration
You can register variables with the CSS Typed OM for transitions, inheritance, etc.
```ts
const definition = cssVarUtils.define({ primaryColor: "#F012BE" });
CSS.registerProperty({
name: definition.getKey("primaryColor"),
syntax: "",
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 stable build hash or random suffix.
```ts
import { createCssVarUtils } from "@crescendolab/css-var-ts";
const ns = process.env.APP_NAMESPACE ?? "app"; // e.g. marketing, analytics
const buildId = process.env.COMMIT_SHA?.slice(0, 7) ?? "dev";
const scopedCssVarUtils = createCssVarUtils({
recordKeyToCssVarKey: (k) =>
`--${ns}-${buildId}-${k}-${Math.random().toString(36).slice(2, 8)}` as const,
});
```
For deterministic builds replace `Math.random()` with a hash of `(ns + buildId + k)`.
2. Strongly recommended: Register core design tokens via `@property` to enforce syntax (e.g. ``, ``) 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.
```ts
const definition = cssVarUtils.define({ accent: "#F012BE" });
definition.cssVarRecord; // { 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)"
```
Each call to `define()` returns an object:
| Key | Type | Description |
| ---------------- | ------------------------- | ------------------------------------------------------------- |
| `cssVarRecord` | original readonly record | Raw tokens you passed in |
| `cssProps` | Record | Object you can spread into style systems to declare variables |
| `getKey(name)` | string | Generated CSS variable name (e.g. `--accent-…`) |
| `getValue(name)` | `var(--token)` | Proper `var()` usage string |
### `createCssVarUtils(options)`
Low‑level factory to customize naming.
```ts
const custom = createCssVarUtils({
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 | Palette + semantic | [`01_basic`](https://github.com/crescendolab-open/css-var-ts/tree/main/packages/examples/src/examples/01_basic/index.stories.tsx) | [Playground](https://crescendolab-open.github.io/css-var-ts/?path=/story/examples-01-basic--playground) |
| Emotion (class) | `@emotion/css` | [`02_integration/01_emotion/01_emotion_css`](https://github.com/crescendolab-open/css-var-ts/tree/main/packages/examples/src/examples/02_integration/01_emotion/01_emotion_css/index.stories.tsx) | [Demo](https://crescendolab-open.github.io/css-var-ts/?path=/story/examples-02-integration-01-emotion-01-emotion-css--story) |
| Emotion (css prop) | `@emotion/react` | [`02_integration/01_emotion/02_css_prop`](https://github.com/crescendolab-open/css-var-ts/tree/main/packages/examples/src/examples/02_integration/01_emotion/02_css_prop/index.stories.tsx) | [Demo](https://crescendolab-open.github.io/css-var-ts/?path=/story/examples-02-integration-01-emotion-02-css-prop--story) |
| MUI | `sx` prop | [`02_integration/02_mui_sx_prop`](https://github.com/crescendolab-open/css-var-ts/tree/main/packages/examples/src/examples/02_integration/02_mui_sx_prop/index.stories.tsx) | [Demo](https://crescendolab-open.github.io/css-var-ts/?path=/story/examples-02-integration-02-mui-sx-prop--story) |
| Advanced | Static custom keys | [`03_advanced/01_staticCssVarKey`](https://github.com/crescendolab-open/css-var-ts/tree/main/packages/examples/src/examples/03_advanced/01_staticCssVarKey.stories.tsx) | [Demo](https://crescendolab-open.github.io/css-var-ts/?path=/story/examples-03-advanced-01-staticcssvarkey--playground) |
| Advanced | `@property` | [`03_advanced/02_@property_atRule`](https://github.com/crescendolab-open/css-var-ts/tree/main/packages/examples/src/examples/03_advanced/02_@property_atRule.stories.tsx) | [Demo](https://crescendolab-open.github.io/css-var-ts/?path=/story/examples-03-advanced-02-property-atrule--at-rule) |
---
## 🤔 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: `createCssVarUtils` + `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](https://github.com/VdustR/template-aio/blob/main/CONTRIBUTING.md).
Suggested areas:
- New integrations (e.g. Tailwind plugin example)
- Additional DX helpers
- Documentation improvements
---
## 📜 License
[Apache-2.0](https://github.com/crescendolab-open/reamgif/blob/main/LICENSE)
Copyright (c) 2025 [Crescendo Lab](https://github.com/crescendolab-open/)
---
Made with ❤️ to make CSS variables first-class citizens in TypeScript.