Appearance
@vibe-labs/design-vue — Package Development Guide
How to build a Vue-layer package for the Vibe Design System. These are Vue 3 components that consume the CSS from design-components-* packages and add behaviour, accessibility, slots, events, and composables.
Package Anatomy
vibe-design-vue-{name}/
├── package.json
├── readme.md
├── tsconfig.json
├── vite.config.ts
└── src/
├── index.ts # barrel — exports components, types, composables
├── types.ts # Vue-level props (extends component-level style props)
├── components/
│ ├── Vibe{Name}.vue # primary component
│ └── Vibe{Sub}.vue # sub-components as needed
└── composables/ # optional — headless logic
└── use{Feature}.tsAfter build:
dist/
├── index.js # ES module bundle
├── index.d.ts # TypeScript declarations (merged)
└── *.d.ts # per-component declarations (from vite-plugin-dts)package.json
json
{
"name": "@vibe-labs/design-vue-{name}",
"version": "0.9.0",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"exports": {
".": "./dist/index.js"
},
"scripts": {
"build": "vite build"
},
"peerDependencies": {
"vue": "^3.5.18"
},
"dependencies": {
"@vibe-labs/core": "*",
"@vibe-labs/design-components-{name}": "*"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"typescript": "^5.9.2",
"vite": "^7.1.2",
"vite-plugin-dts": "^4.5.4",
"vue": "^3.5.18"
}
}Key differences from component-level packages
- Single JS export — no separate CSS export (CSS comes from the
design-components-*peer) - Vite build — not the
rimraf && ncp && tsc && tsxpipeline - Vue as peerDependency — never bundled
- All
@vibe-labs/*externalized — Rollupexternal: [/^@vibe-labs\//] - Dependencies list the component-level CSS package and
@vibe-labs/core(for shared utils likeColorFromString)
Not all Vue packages need a corresponding design-components-* package. Some are pure logic (e.g. forms) or compose other Vue packages without adding new CSS.
Build Pipeline
json
"build": "vite build"That's it. Vite handles everything — Vue SFC compilation, TypeScript, tree-shaking, and declaration generation via vite-plugin-dts.
vite.config.ts
Same structure for every Vue package — update entry, name, and fileName:
ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import dts from "vite-plugin-dts";
import path from "path";
export default defineConfig({
build: {
sourcemap: true,
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "VibeDesignVue{Name}",
formats: ["es"],
fileName: "index",
},
rollupOptions: {
external: ["vue", /^@vibe-labs\//],
output: {
globals: {
vue: "Vue",
},
},
},
},
plugins: [
vue(),
dts({
insertTypesEntry: true,
copyDtsFiles: true,
}),
],
});Critical: externals
external: ["vue", /^@vibe-labs\//] ensures Vue and all sibling packages are never bundled — they resolve at runtime from the consuming app. This keeps bundles tiny and avoids duplicate Vue instances.
tsconfig.json
json
{
"compilerOptions": {
"outDir": "dist",
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Node",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {
"@vibe-labs/core": ["../../../vibe-core/src"],
"@vibe-labs/design-components-{name}/types": ["../../components/vibe-design-components-{name}/types/index"]
}
},
"include": ["src/**/*.ts", "src/**/*.vue"],
"exclude": ["dist", "vite.config.ts"]
}Key differences from component-level tsconfig
"lib"includesDOMandDOM.Iterable— needed for Vue template refs, event handlers, DOM APIs"include"covers*.vuefiles — Vite + vue-tsc handle SFC type extraction"paths"maps sibling packages — for IDE resolution during development (Vite resolves these at build via the monorepo workspace)
Barrel: src/index.ts
Exports components, types, and composables:
ts
/* ── Components ── */
export { default as VibeBadge } from "./components/VibeBadge.vue";
export { default as VibeBadgeCount } from "./components/VibeBadgeCount.vue";
export { default as VibeDot } from "./components/VibeDot.vue";
/* ── Types (re-exported from components package + local) ── */
export type { VibeBadgeProps, VibeBadgeCountProps, VibeDotProps } from "./types";
export type { BadgeVariant, BadgeSize, BadgeStyleProps, BadgeGroupStyleProps } from "./types";
export { BadgeVariants, BadgeSizes } from "./types";
/* ── Composables (if any) ── */
// export { useXxx } from "./composables/useXxx";Re-export rules
- Component-level style types (
BadgeVariant,BadgeSize, etc.) — re-export from./types.tsso consumers get everything from one import path - Runtime constants (
BadgeVariants,BadgeSizes) — re-export for prop validation and runtime use - Vue-level prop interfaces (
VibeBadgeProps) — export astypeonly
Types: src/types.ts
The type bridge between the component-level CSS types and Vue-level props.
ts
// Re-export everything from the component-level types
import type { BadgeVariant, BadgeSize, BadgeStyleProps, BadgeGroupStyleProps } from "@vibe-labs/design-components-badges/types";
export type { BadgeVariant, BadgeSize, BadgeStyleProps, BadgeGroupStyleProps } from "@vibe-labs/design-components-badges/types";
export { BadgeVariants, BadgeSizes } from "@vibe-labs/design-components-badges/types";
/* ── Vue-level props ── */
export interface VibeBadgeProps extends Omit<BadgeStyleProps, "dot"> {
/** Text content of the badge */
label?: string;
/** Override background color via inline style */
bgColor?: string;
/** Override text color — auto-contrasted from bgColor if not set */
fgColor?: string;
/** Auto-generate bg/fg from label text */
autoColor?: boolean;
/** Show remove button, emit `dismiss` */
dismissible?: boolean;
}The type layering pattern
design-components-badges/types → style props (CSS-level, framework-agnostic)
↓ extends
design-vue-badges/types → Vue props (adds behaviour, events, slots)
↓ used by
VibeBadge.vue → defineProps<VibeBadgeProps>()Vue props extend the component-level style props, adding:
- Behavioural props (
dismissible,autoColor,loading, etc.) - Override props (
bgColor,fgColor,color) - Content props (
label,message,title) - Any
Omit<>to exclude CSS-only concerns that don't apply at the Vue level
Component Authoring
SFC structure
vue
<script setup lang="ts">
import { computed } from "vue";
import { ColorFromString } from "@vibe-labs/core";
import type { VibeBadgeProps } from "../types";
const props = withDefaults(defineProps<VibeBadgeProps>(), {
variant: "accent-subtle",
size: "md",
pill: true,
interactive: false,
dismissible: false,
autoColor: false,
});
const emit = defineEmits<{
dismiss: [];
}>();
defineOptions({ inheritAttrs: false });
/* ── Computed ── */
// ...
</script>
<template>
<span
class="badge"
:data-variant="variant"
:data-size="size"
:data-pill="pill || undefined"
:data-interactive="interactive || undefined"
:data-removable="dismissible || undefined"
:style="colorOverride"
:role="interactive ? 'button' : undefined"
:tabindex="interactive ? 0 : undefined"
v-bind="$attrs"
@keydown="onKeydown"
>
<slot name="left" />
<span class="badge-label"
><slot>{{ label }}</slot></span
>
<slot name="right" />
</span>
</template>Conventions
CSS class = component-level class name. The root element always gets the base class (e.g. badge, btn, card). All styling comes from the design-components-* CSS — no <style> blocks in Vue SFCs.
Data attributes for variants/flags. Bind with :data-variant="variant", :data-size="size", etc. For boolean flags, use :data-flag="flag || undefined" — undefined removes the attribute entirely (falsy data attributes like data-dot="false" would still match [data-dot] selectors).
defineOptions({ inheritAttrs: false }) + v-bind="$attrs" — explicit control over where attrs land (always on the root semantic element, not an outer wrapper).
withDefaults — set sensible defaults for every optional prop. These should match the documented defaults in the contents reference.
Events via defineEmits — typed emit signatures, no runtime validation overhead.
No <style> blocks. All visual styling comes from the component-level CSS package. Vue components are purely structural + behavioural. The only inline styles are dynamic overrides (e.g. colorOverride for autoColor/bgColor).
Data Attribute Binding Patterns
vue
<!-- Enum variant — always bound -->
:data-variant="variant" :data-size="size" :data-align="align"
<!-- Boolean flag — undefined removes the attribute -->
:data-pill="pill || undefined" :data-interactive="interactive || undefined" :data-loading="loading || undefined" :data-disabled="disabled ||
undefined"
<!-- ARIA state — prefer native/ARIA over custom data attrs -->
:aria-selected="selected" :aria-expanded="expanded" :aria-disabled="disabled || undefined" :disabled="disabled || undefined"
<!-- Conditional role -->
:role="interactive ? 'button' : undefined" :tabindex="interactive ? 0 : undefined"Composables
Composables provide headless, reusable logic. They live in src/composables/ and are exported from the barrel.
Common patterns
State management — useModal(), useToast(), usePagination()DOM interaction — useClickOutside(), useFocusTrap(), useScrollLock()Data processing — useTableSort(), useTableSelection(), useUploadQueue()Input helpers — useInputField(), useAutoResize(), useSliderDrag()
Composable conventions
- Return reactive refs and computed properties, not raw values
- Accept
MaybeRefOrGetter<T>for inputs when flexibility is needed - Clean up side effects in
onUnmounted/onScopeDispose - Prefix with
use— always
Provide/Inject for Compound Components
Compound component groups (dropdown, tabs, form, list, menu) use provide/inject to share context between parent and children without prop drilling.
ts
// Parent provides context
const context = {
activeTab: ref("first"),
registerTab: (name: string) => { ... },
selectTab: (name: string) => { ... },
};
provide(TABS_INJECTION_KEY, context);
// Child injects it
const ctx = inject(TABS_INJECTION_KEY);Children should fail gracefully if used outside their parent — check for undefined inject and either warn or fall back to standalone behaviour.
Accessibility
Baseline requirements
- Semantic HTML — use
<button>,<a>,<nav>,<input>, not generic<div>with roles bolted on - ARIA attributes —
aria-label,aria-selected,aria-expanded,aria-disabled,roleas needed - Keyboard navigation — every interactive component must be keyboard-operable
- Focus management — modals trap focus, dropdowns restore focus on close, menus support arrow keys
- Screen reader announcements —
role="status"for live updates (toasts, spinners, upload progress)
ARIA over custom attributes
Prefer native ARIA for state that assistive tech needs to know about:
vue
<!-- Good: ARIA state -->
:aria-selected="selected" :aria-expanded="expanded" :aria-current="active ? 'page' : undefined"
<!-- Good: CSS hooks via ARIA (component-level CSS targets these too) -->
.tab[aria-selected="true"] { ... }
<!-- Avoid: custom data-attr for state that AT needs -->
:data-selected="selected"
<!-- AT can't see this -->Use data-* attributes for visual-only modifiers that don't carry semantic meaning (e.g. data-variant, data-size, data-pill).
Readme Structure
markdown
# @vibe-labs/design-vue-{name}
One-line description.
## Installation
\```ts
import { VibeComponent } from "@vibe-labs/design-vue-{name}";
\```
Requires the CSS layer from `@vibe-labs/design-components-{name}`.
## Components
### VibeComponent
Description.
#### Usage
Show 4-6 practical examples covering common use cases.
#### Props
Table: Prop | Type | Default | Description
#### Events
Table: Event | Payload | Description
#### Slots
Table: Slot | Description
### (repeat for each component)
## Composables (if any)
### useFeature(options)
Description + returns.
## Dependencies
| Package | Purpose |
| ------------------------------------- | ------------------- |
| `@vibe-labs/core` | Shared utilities |
| `@vibe-labs/design-components-{name}` | CSS tokens + styles |
## Build
\```bash
npm run build
\```Cross-Package Dependencies
| Dependency type | Declared where | Example |
|---|---|---|
| CSS tokens + styles | package.json dependencies | "@vibe-labs/design-components-badges": "*" |
| Shared core utils | package.json dependencies | "@vibe-labs/core": "*" |
| Vue | package.json peerDependencies | "vue": "^3.5.18" |
| Sibling Vue packages | package.json dependencies | "@vibe-labs/design-vue-images": "*" |
All @vibe-labs/* | Rollup externals | Never bundled — resolved at runtime |
The CSS from design-components-* must be imported by the consuming app (or the umbrella). Vue packages don't import CSS themselves — they just apply the class names and data attributes that the CSS targets.
Quick Checklist: New Vue Package
- Create
vibe-design-vue-{name}/directory - Add
package.json(copy template, update name + dependencies) - Add
tsconfig.json(copy template, updatepathsfor sibling packages) - Add
vite.config.ts(copy template, updateentry,name,fileName) - Create
src/types.ts— re-export component-level types + define Vue-level prop interfaces - Create
src/components/Vibe{Name}.vue— SFC with<script setup>, typed props, no<style> - Create
src/index.tsbarrel — export components, types, composables - Add composables in
src/composables/if the component has reusable headless logic - Write
readme.mdwith usage examples, props/events/slots tables, and dependency list - Run
npm run buildand verify dist output - Add the package to the umbrella
@vibe-labs/design-vueimports