Appearance
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-resetand@vibe-labs/design-filterscontain hand-authored CSS with no tokens and no generated utilities. Their build scripts only write thedist/index.cssbarrel. 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.utilitiesLayer 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- Clean — remove stale dist
- Copy — copy source CSS (tokens) into dist as-is
- Generate — create
{name}.g.cssand overwritedist/index.cssbarrel
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:rootscope - 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
- Create
vibe-design-{name}/directory - Add
package.json(copy template, update name) - Create
src/{name}.csswith tokens in@layer vibe.tokens - Create
src/index.cssbarrel - Create
scripts/build.mjsgenerating utilities into@layer vibe.utilities - Write
readme.md,developer.md,usage.md,contents.md - Run
npm run buildand verify dist - Add the package to umbrella
@vibe-labs/designimports
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).