Skip to content

@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}.ts

After 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 && tsx pipeline
  • Vue as peerDependency — never bundled
  • All @vibe-labs/* externalized — Rollup external: [/^@vibe-labs\//]
  • Dependencies list the component-level CSS package and @vibe-labs/core (for shared utils like ColorFromString)

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" includes DOM and DOM.Iterable — needed for Vue template refs, event handlers, DOM APIs
  • "include" covers *.vue files — 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.ts so 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 as type only

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 managementuseModal(), useToast(), usePagination()DOM interactionuseClickOutside(), useFocusTrap(), useScrollLock()Data processinguseTableSort(), useTableSelection(), useUploadQueue()Input helpersuseInputField(), 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 attributesaria-label, aria-selected, aria-expanded, aria-disabled, role as 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 announcementsrole="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 typeDeclared whereExample
CSS tokens + stylespackage.json dependencies"@vibe-labs/design-components-badges": "*"
Shared core utilspackage.json dependencies"@vibe-labs/core": "*"
Vuepackage.json peerDependencies"vue": "^3.5.18"
Sibling Vue packagespackage.json dependencies"@vibe-labs/design-vue-images": "*"
All @vibe-labs/*Rollup externalsNever 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

  1. Create vibe-design-vue-{name}/ directory
  2. Add package.json (copy template, update name + dependencies)
  3. Add tsconfig.json (copy template, update paths for sibling packages)
  4. Add vite.config.ts (copy template, update entry, name, fileName)
  5. Create src/types.ts — re-export component-level types + define Vue-level prop interfaces
  6. Create src/components/Vibe{Name}.vue — SFC with <script setup>, typed props, no <style>
  7. Create src/index.ts barrel — export components, types, composables
  8. Add composables in src/composables/ if the component has reusable headless logic
  9. Write readme.md with usage examples, props/events/slots tables, and dependency list
  10. Run npm run build and verify dist output
  11. Add the package to the umbrella @vibe-labs/design-vue imports