Skip to content

@vibe-labs/design — Package Development Guide

How to build a design-level package for the Vibe Design System. Follow this structure exactly — it's what every package uses.


Package Anatomy

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)
    └── (optional subdirs)  # for opt-in modules (scales/, palettes/, etc.)

After build:

dist/
├── index.css              # barrel (copied from src, then overwritten with correct imports)
├── {name}.css             # token definitions (copied from src)
├── {name}.g.css           # generated utility classes
└── (optional subdirs)     # if the package has opt-in exports

package.json

Standard (no optional exports)

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"
  },
  "dependencies": {},
  "devDependencies": {
    "ncp": "^2.0.0",
    "rimraf": "^6.1.2"
  }
}

With optional exports (e.g. colors)

Add additional export entries pointing to dist paths. Each opt-in module gets its own export:

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

Consumers import opt-ins individually:

css
@import "@vibe-labs/design-colors";
@import "@vibe-labs/design-colors/scales/main/blue";

Build Pipeline

Every package uses the same build command in package.json:

json
"build": "rimraf dist && ncp src dist && node ./scripts/build.mjs"
  1. Cleanrimraf dist removes stale output
  2. Copyncp src dist copies all source CSS (tokens) into dist as-is
  3. Generatebuild.mjs creates {name}.g.css (generated utilities) and overwrites index.css with correct barrel imports

Source CSS: Token Definitions

Tokens live in src/{name}.css (or multiple files in subdirectories). All tokens are defined inside @layer vibe.tokens on :root:

css
@layer vibe.tokens {
  :root {
    /* Semantic tokens referencing lower-level tokens */
    --surface-background: var(--color-neutral-950);
    --surface-base: var(--color-neutral-900);

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

Rules

  • Always @layer vibe.tokens
  • Always :root scope
  • Semantic tokens reference other packages' primitives via var(--...)
  • Primitive tokens use direct values (px, rem, hex, rgba, etc.)
  • Document cross-package dependencies in the readme

Build Script: Generating Utilities

scripts/build.mjs generates utility classes into @layer vibe.utilities. Pattern:

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`;
}

/* ── Category heading ── */

function generateSomethingUtilities() {
  let output = "";
  // Map tokens to utility classes
  output += `.util-name { property: var(--token-name); }\n`;
  return output;
}

/* ── Write all ── */

const all = [generateSomethingUtilities(), generateOtherUtilities()].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 mapping (most common):

js
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`;
}

Named utilities (small finite set):

js
output += `.backdrop-blur-none { backdrop-filter: none; }\n`;
output += `.backdrop-blur-sm { backdrop-filter: blur(var(--blur-sm)); }\n`;

Semantic aliases (map short class to semantic token):

js
output += `.bg-background { background-color: var(--surface-background); }\n`;
output += `.bg-base { background-color: var(--surface-base); }\n`;

Enum-style utilities (CSS values iterated):

js
for (const m of ["normal", "multiply", "screen", "overlay", "darken", "lighten"]) {
  output += `.bg-blend-${m} { background-blend-mode: ${m}; }\n`;
}

Barrel: index.css

Source (src/index.css)

Imports the hand-authored token files only:

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

Or for packages with multiple source files:

css
@import "./scales/neutral/scales-neutral.css";
@import "./scales/scales-status.css";

Dist (dist/index.css) — overwritten by build

The build script overwrites this to include both tokens and generated utilities:

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

CSS Layer Order

All packages participate in the global layer stack:

vibe.reset → vibe.tokens → vibe.utilities → vibe.components → vibe.theme → vibe.accessibility
  • Token files@layer vibe.tokens
  • Generated utilities@layer vibe.utilities
  • Unlayered CSS (tenant overrides) wins over all layers automatically

Readme Structure

Every package readme follows this template:

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

One-line description of what this package provides.

## Usage

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

## Contents

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

Document every token with its default value.

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

List every generated utility class.

## Dist Structure

| File           | Description                       |
| -------------- | --------------------------------- |
| `index.css`    | Barrel — imports both files below |
| `{name}.css`   | Token definitions                 |
| `{name}.g.css` | Generated utility classes         |

## Dependencies

List which tokens from other packages are required
(e.g. `--color-neutral-*` from `@vibe-labs/design-colors`).

## Build

\```bash
npm run build
\```

Cross-Package Dependencies

Packages reference each other's tokens via CSS custom properties. These are runtime dependencies (the tokens must be loaded in the cascade), not npm dependencies.

Document them in the readme but don't add them to package.json dependencies — the umbrella @vibe-labs/design package handles import order.

Common dependency chains:

  • surfaces requires --color-neutral-* (colors) and --text-* (typography)
  • elevation requires --color-accent (theme)
  • forms requires tokens from borders, typography, and colors

Variant System

Component CSS (at the vibe.components layer) uses data-attribute selectors by default:

html
<button class="btn" data-variant="accent" data-size="lg">Click me</button>

Flat class mode also available: .btn-accent · .btn-lg

This is relevant context but not something design-level packages implement — it's handled at the component layer.


Quick Checklist: New Package

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