Skip to content

Build Guide — Design Token Packages

How to create a new @vibe-labs/design-{name} package. These packages define CSS custom property tokens and generate single-property utility classes.

Special cases: @vibe-labs/design-reset and @vibe-labs/design-filters contain hand-authored CSS with no tokens and no generated utilities. Their build scripts only write the dist/index.css barrel. They do not follow this template.


Architecture

@vibe-labs/design                    ← Umbrella: layer order + all design-* packages
  ├── @vibe-labs/design-reset        ← CSS reset in @layer vibe.reset
  ├── @vibe-labs/design-filters      ← Compound filter effects in @layer vibe.utilities
  └── @vibe-labs/design-{name}       ← Tokens in @layer vibe.tokens + utilities in @layer vibe.utilities

Layer cascade order:

css
@layer vibe.reset, vibe.tokens, vibe.utilities, vibe.components, vibe.theme, vibe.accessibility;

Unlayered CSS (tenant overrides) always wins over all layers.


Directory Structure

vibe-design-{name}/
├── package.json
├── readme.md
├── scripts/
│   └── build.mjs          # generates utility CSS from tokens
└── src/
    ├── index.css           # barrel — imports all source CSS
    └── {name}.css          # token definitions (@layer vibe.tokens)

After build → dist/ contains: index.css (barrel), {name}.css (tokens), {name}.g.css (generated utilities).


package.json

json
{
  "name": "@vibe-labs/design-{name}",
  "version": "0.1.0",
  "private": false,
  "type": "module",
  "files": ["dist"],
  "style": "./dist/index.css",
  "exports": {
    ".": { "default": "./dist/index.css" }
  },
  "sideEffects": ["*.css"],
  "scripts": {
    "build": "rimraf dist && ncp src dist && node ./scripts/build.mjs"
  },
  "devDependencies": {
    "ncp": "^2.0.0",
    "rimraf": "^6.1.2"
  }
}

For packages with sub-exports (e.g. individual colour scales):

json
"exports": {
  ".": { "default": "./dist/index.css" },
  "./scales/main/blue": { "default": "./dist/scales/main/scales-main-blue.css" }
}

Build Pipeline

rimraf dist → ncp src dist → node ./scripts/build.mjs
  1. Clean — remove stale dist
  2. Copy — copy source CSS (tokens) into dist as-is
  3. Generate — create {name}.g.css and overwrite dist/index.css barrel

Token Definitions (src/{name}.css)

All tokens in @layer vibe.tokens on :root:

css
@layer vibe.tokens {
  :root {
    /* Semantic tokens reference other packages' primitives */
    --surface-background: var(--color-neutral-950);
    --surface-base: var(--color-neutral-900);

    /* Primitive tokens use direct values */
    --blur-sm: 4px;
    --blur-md: 8px;
    --opacity-50: 0.5;
  }
}

Token rules:

  • Always @layer vibe.tokens, always :root scope
  • Semantic tokens reference other packages via var(--...)
  • Primitive tokens use direct values (px, rem, hex, rgba)
  • Document cross-package token dependencies in readme (these are runtime deps, not npm deps)

Source Barrel (src/index.css)

css
@import "./{name}.css";

The build script overwrites dist/index.css to add the generated file:

css
@import "./{name}.css";
@import "./{name}.g.css";

Build Script (scripts/build.mjs)

js
import fs from "fs";
import path from "path";

const distDir = path.resolve("dist");

// Wrap all generated CSS in the utilities layer
function l(txt) {
  return `@layer vibe.utilities {\n${txt}}\n`;
}

/* ── Token-to-class mapping (most common) ── */
function generateOpacityUtilities() {
  let output = "";
  const steps = [0, 5, 10, 20, 25, 50, 75, 80, 90, 95, 100];
  for (const n of steps) {
    output += `.opacity-${n} { opacity: var(--opacity-${n}); }\n`;
  }
  return output;
}

/* ── Named utilities (small finite set) ── */
function generateBlurUtilities() {
  let output = "";
  output += `.backdrop-blur-none { backdrop-filter: none; }\n`;
  output += `.backdrop-blur-sm { backdrop-filter: blur(var(--blur-sm)); }\n`;
  output += `.backdrop-blur-md { backdrop-filter: blur(var(--blur-md)); }\n`;
  return output;
}

/* ── Semantic aliases ── */
function generateSurfaceUtilities() {
  let output = "";
  output += `.bg-background { background-color: var(--surface-background); }\n`;
  output += `.bg-base { background-color: var(--surface-base); }\n`;
  return output;
}

/* ── Enum-style utilities ── */
function generateBlendModes() {
  let output = "";
  for (const m of ["normal", "multiply", "screen", "overlay", "darken", "lighten"]) {
    output += `.bg-blend-${m} { background-blend-mode: ${m}; }\n`;
  }
  return output;
}

/* ── Write all ── */
const all = [generateOpacityUtilities(), generateBlurUtilities(), generateSurfaceUtilities(), generateBlendModes()].join("");

fs.writeFileSync(path.join(distDir, "{name}.g.css"), l(all));
fs.writeFileSync(path.join(distDir, "index.css"), `@import "./{name}.css";\n@import "./{name}.g.css";\n`);

Generation patterns:

  • Token-to-class — loop a scale array, map each to var(--token-name)
  • Named — finite set of explicit rules
  • Semantic aliases — short class → semantic token
  • Enum — iterate CSS keyword values

Checklist

  1. Create vibe-design-{name}/ directory
  2. Add package.json (copy template, update name)
  3. Create src/{name}.css with tokens in @layer vibe.tokens
  4. Create src/index.css barrel
  5. Create scripts/build.mjs generating utilities into @layer vibe.utilities
  6. Write readme.md, developer.md, usage.md, contents.md
  7. Run npm run build and verify dist
  8. Add the package to umbrella @vibe-labs/design imports

Readme Template

markdown
# @vibe-labs/design-{name}

One-line description.

## Usage

\`\`\`css
@import "@vibe-labs/design-{name}";
\`\`\`

## Contents

### Tokens (`{name}.css`)

Document every token with default values in tables.

### Generated Utilities (`{name}.g.css`)

List every generated utility class.

## Dist Structure

| File           | Description         |
| -------------- | ------------------- |
| `index.css`    | Barrel              |
| `{name}.css`   | Token definitions   |
| `{name}.g.css` | Generated utilities |

## Dependencies

List required tokens from other packages (runtime, not npm).

Vibe