Appearance
@vibe-labs/design-components — Package Development Guide
How to build a component-level package for the Vibe Design System. Follow this structure exactly — it's what every component package uses.
Package Anatomy
vibe-design-components-{name}/
├── package.json
├── readme.md
├── tsconfig.json
├── scripts/
│ └── generate.ts # generates component CSS from tokens + types
├── src/
│ └── {name}.css # token definitions (@layer vibe.tokens)
└── types/
└── index.ts # runtime constants + TypeScript typesAfter build:
dist/
├── index.css # barrel (tokens + generated styles)
├── {name}.css # token definitions (copied from src)
├── {name}.g.css # generated component styles
├── index.js # compiled types (runtime constants)
├── index.d.ts # type declarations
├── index.d.ts.map # declaration source map
└── index.js.map # JS source mappackage.json
json
{
"name": "@vibe-labs/design-components-{name}",
"version": "0.1.0",
"private": false,
"type": "module",
"files": ["dist"],
"style": "./dist/index.css",
"exports": {
".": {
"default": "./dist/index.css"
},
"./types": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"sideEffects": ["*.css"],
"scripts": {
"build": "rimraf dist && ncp src dist && tsc --rootDir types --outDir dist && tsx ./scripts/generate.ts --mode attr"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^25.2.3",
"ncp": "^2.0.0",
"rimraf": "^6.1.2",
"tsx": "^4.21.0",
"typescript": "^5.5.0"
}
}Key differences from design-level packages
- Two exports:
.for CSS,./typesfor TypeScript constants + types - Build command includes
tsc(compilestypes/→dist/) and usestsx(runs the TypeScript generate script) --mode attrflag tells the generator to emit data-attribute selectors (the default selector strategy)
Build Pipeline
Every component package uses the same build command in package.json:
json
"build": "rimraf dist && ncp src dist && tsc --rootDir types --outDir dist && tsx ./scripts/generate.ts --mode attr"- Clean —
rimraf distremoves stale output - Copy —
ncp src distcopies token CSS into dist as-is - Compile types —
tsc --rootDir types --outDir distemits.js,.d.ts, and source maps - Generate styles —
tsx ./scripts/generate.ts --mode attrcreates{name}.g.cssand overwritesindex.css
tsconfig.json
Same for every component package:
json
{
"compilerOptions": {
"outDir": "dist",
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "Node",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {}
},
"include": ["types/**/*.ts"],
"exclude": ["node_modules", "dist"]
}Source CSS: Token Definitions
Tokens live in src/{name}.css, defined inside @layer vibe.tokens on :root — same as design-level packages:
css
@layer vibe.tokens {
:root {
/* ── Sizing ── */
--badge-height-sm: 1.25rem;
--badge-height-md: 1.5rem;
--badge-height-lg: 1.75rem;
--badge-px-sm: var(--space-1);
--badge-px-md: var(--space-2);
/* ── Appearance ── */
--badge-radius: var(--radius-full);
--badge-font-weight: var(--font-semibold, 600);
--badge-bg: var(--surface-elevated);
}
}Token naming convention
All component tokens are prefixed with the component name:
--{component}-{property} → --badge-radius
--{component}-{property}-{size} → --badge-height-sm
--{component}-{sub}-{property} → --dropdown-item-height
--{component}-{sub}-{property}-{size} → --list-item-height-sm
--{component}-{sub}-{property}-{state} → --menu-item-hover-bgFallback values
Use var(--token, fallback) only when a design-level token might not be loaded. For tokens you own, direct values are fine:
css
--badge-font-weight: var(--font-semibold, 600); /* fallback for safety */
--badge-height-sm: 1.25rem; /* own token, direct value */TypeScript Types
Types live in types/index.ts. Every component exports:
- Runtime
constarrays — used by the generate script and consumable by Vue/JS at runtime - Derived literal types — extracted from the const arrays
- Style prop interfaces — CSS-level props, framework-agnostic
ts
// Runtime constants
export const BadgeSizes = ["sm", "md", "lg"] as const;
export const BadgeVariants = [
"accent",
"success",
"warning",
"danger",
"info",
"accent-subtle",
"success-subtle",
"warning-subtle",
"danger-subtle",
"info-subtle",
"outline",
] as const;
// Derived types
export type BadgeSize = (typeof BadgeSizes)[number];
export type BadgeVariant = (typeof BadgeVariants)[number];
// Style prop interfaces (CSS-level, framework-agnostic)
export interface BadgeStyleProps {
variant?: BadgeVariant;
size?: BadgeSize;
dot?: boolean;
pill?: boolean;
square?: boolean;
interactive?: boolean;
removable?: boolean;
}
// Sub-component style props as needed
export interface BadgeGroupStyleProps {
label?: string;
}Naming conventions
| Export | Pattern | Example |
|---|---|---|
| Size array | {Component}Sizes | BadgeSizes |
| Variant array | {Component}Variants | BadgeVariants |
| Other arrays | {Component}{Dimension}s | DropdownAlignments, SliderThumbShapes |
| Size type | {Component}Size | BadgeSize |
| Variant type | {Component}Variant | BadgeVariant |
| Style props | {Component}StyleProps | BadgeStyleProps |
| Sub-component props | {Component}{Sub}StyleProps | BadgeGroupStyleProps |
Style prop interfaces are CSS-level only — they describe what data-attributes and modifiers the CSS supports, not Vue component props. The Vue layer wraps these with behaviour props (events, slots, accessibility) at the @vibe-labs/ui-* level.
Generate Script
scripts/generate.ts creates the component CSS. It imports types from the types/ directory and selector helpers from the shared .build/ directory.
Structure
ts
import fs from "fs";
import path from "path";
import { base, variant, flag } from "../../../.build/selectors";
import { ComponentSizes, type ComponentVariant } from "../types/index";
const distDir = path.resolve("dist");
// Wrap all generated CSS in the components layer
function layer(txt: string): string {
return `@layer vibe.components {\n${txt}}\n`;
}
// Generate a single CSS rule
function rule(selector: string, ...declarations: string[]): string {
return `${selector} {\n${declarations.map((d) => ` ${d};`).join("\n")}\n}\n`;
}
/* ── Base ── */
function generateBase(): string { ... }
/* ── Sizes ── */
function generateSizes(): string { ... }
/* ── Variants ── */
function generateVariants(): string { ... }
/* ── Modifiers ── */
function generateModifiers(): string { ... }
/* ── Write ── */
const all = [
generateBase(),
generateSizes(),
generateVariants(),
generateModifiers(),
].join("\n");
fs.writeFileSync(path.join(distDir, "{name}.g.css"), layer(all));
fs.writeFileSync(
path.join(distDir, "index.css"),
`@import "./{name}.css";\n@import "./{name}.g.css";\n`
);Shared selector helpers
The ../../../.build/selectors module provides three functions that respect the --mode flag:
| Helper | Usage | Output (attr mode) |
|---|---|---|
base(name) | base("badge") | .badge |
variant(name, dimension, value) | variant("badge", "size", "sm") | .badge[data-size="sm"] |
flag(name, modifier) | flag("badge", "dot") | .badge[data-dot] |
In flat-class mode these would emit .badge-sm, .badge-dot, etc. — but attr mode is the default.
Generation patterns
Base styles — the foundational selector for the component:
ts
function generateBase(): string {
return rule(
base("badge"),
"display: inline-flex",
"align-items: center",
"justify-content: center",
"gap: var(--space-1)",
"font-weight: var(--badge-font-weight)",
"border-radius: var(--badge-radius)",
"background-color: var(--badge-bg)",
"color: var(--badge-color)",
);
}Size variants — iterate the sizes array from types:
ts
function generateSizes(): string {
return BadgeSizes.map((s) =>
rule(
variant("badge", "size", s),
`height: var(--badge-height-${s})`,
`padding-left: var(--badge-px-${s})`,
`padding-right: var(--badge-px-${s})`,
`font-size: var(--badge-font-size-${s})`,
),
).join("");
}Named variants — map from a typed record to CSS:
ts
const variantStyles: Record<BadgeVariant, { bg: string; color: string; border?: string }> = {
accent: { bg: "var(--color-accent)", color: "var(--color-accent-contrast)" },
outline: { bg: "transparent", color: "var(--text-secondary)", border: "var(--border-default)" },
// ...
};
function generateVariants(): string {
return (Object.entries(variantStyles) as [BadgeVariant, ...][])
.map(([name, vals]) =>
rule(
variant("badge", "variant", name),
`background-color: ${vals.bg}`,
`color: ${vals.color}`,
...(vals.border ? [`border-color: ${vals.border}`] : []),
),
)
.join("");
}Boolean flag modifiers — standalone data-attributes:
ts
function generateModifiers(): string {
return [
rule(flag("badge", "dot"), "width: 0.5rem", "height: 0.5rem", "padding: 0", "font-size: 0"),
rule(
flag("badge", "interactive"),
"cursor: pointer",
"transition-property: background-color, border-color, opacity",
"transition-duration: var(--transition-duration, var(--duration-normal))",
),
// Pseudo-state on a flagged selector
rule(`${flag("badge", "interactive")}:hover`, "opacity: var(--opacity-80, 0.8)"),
].join("");
}Sub-components — separate base selectors for child elements:
ts
function generateGroup(): string {
return rule(base("badge-group"), "display: flex", "flex-wrap: wrap", "gap: var(--space-1)", "align-items: center");
}Pseudo-states and compound selectors
For hover, focus, disabled, and ARIA states, string-concatenate onto the selector helper output:
ts
// Hover on a flagged element
rule(`${flag("badge", "interactive")}:hover`, "opacity: 0.8");
// Disabled via attribute or ARIA
rule(`${base("btn")}:disabled, ${base("btn")}[aria-disabled]`, "opacity: 0.5");
// ARIA selection (used by tabs, menus, dropdowns)
rule(`${base("tab")}[aria-selected="true"]`, "color: var(--tab-active-color)");
// Nested child when parent has state
rule(`${flag("list", "hoverable")} ${base("list-item")}:hover`, "background-color: var(--list-item-hover-bg)");CSS Layer
All generated component CSS lives in @layer vibe.components:
vibe.reset → vibe.tokens → vibe.utilities → vibe.components → vibe.theme → vibe.accessibilityComponent styles sit above utilities (so they can use utility tokens) and below theme (so tenant overrides win).
Variant System
Components use data-attribute selectors by default:
html
<button class="btn" data-variant="primary" data-size="lg">Click</button> <span class="badge" data-variant="accent" data-dot></span>data-variant— visual variant (accent, success, danger, etc.)data-size— size variant (sm, md, lg)- Boolean flags —
data-dot,data-interactive,data-loading, etc. (presence = true) - State —
aria-selected,aria-current,:disabled(prefer native/ARIA over custom attributes)
Flat class mode (.badge-accent, .badge-sm) is also supported via the selector helpers but attr mode is the default.
Readme Structure
Every component package readme follows this template:
markdown
# @vibe-labs/design-components-{name}
One-line description.
## Usage
\```css
@import "@vibe-labs/design-components-{name}";
\```
\```ts
import { ComponentSizes, ComponentVariants } from "@vibe-labs/design-components-{name}/types";
import type { ComponentSize, ComponentVariant, ComponentStyleProps } from "@vibe-labs/design-components-{name}/types";
\```
## Contents
### Tokens (`{name}.css`)
Document every token with default values, ideally in tables grouped by category.
### Generated Styles (`{name}.g.css`)
Document all classes, variants, sizes, modifiers, sub-components, and state selectors.
### TypeScript Types (`types/`)
Show the exported constants, types, and interfaces.
## Dist Structure
| File | Description |
| ------------------------- | ------------------------------------------ |
| `index.css` | Barrel — imports tokens + generated styles |
| `{name}.css` | Token definitions |
| `{name}.g.css` | Generated component styles |
| `index.js` / `index.d.ts` | TypeScript types + runtime constants |
## Dependencies
List required tokens from other packages.
## Build
\```bash
npm run build
\```Cross-Package Dependencies
Component packages reference design-level tokens via CSS custom properties. These are runtime dependencies — the design umbrella must be loaded first in the cascade.
Don't add design packages to package.json dependencies. The umbrella @vibe-labs/design-components handles import order.
The generate script imports from types/ within the same package. The shared .build/selectors module is accessed via relative path (../../../.build/selectors).
Quick Checklist: New Component Package
- Create
vibe-design-components-{name}/directory - Add
package.json(copy template, update name) - Add
tsconfig.json(copy verbatim) - Create
types/index.tswith const arrays, derived types, and style prop interfaces - Create
src/{name}.csswith component tokens in@layer vibe.tokens - Create
scripts/generate.tsimporting types + selector helpers - Implement generators: base → sizes → variants → modifiers → sub-components
- Write
readme.mddocumenting all tokens, classes, and types - Run
npm run buildand verify dist output - Add the package to the umbrella
@vibe-labs/design-componentsimports