Appearance
Build Guide — Vue Component Packages
How to create a new @vibe-labs/design-vue-{name} package. Vue 3 components that consume CSS from design-components-* and add behaviour, accessibility, slots, events, and composables. These packages contain zero CSS.
Directory Structure
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
└── composables/ # optional — headless logic
└── use{Feature}.tspackage.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)
- Vite build (not rimraf + ncp + tsc + tsx)
- Vue as
peerDependency— never bundled - All
@vibe-labs/*externalized via rollup
vite.config.ts
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\//],
output: { globals: { vue: "Vue" } },
},
},
plugins: [vue(), dts({ insertTypesEntry: true, copyDtsFiles: true })],
});Critical: external: ["vue", /^@vibe\//] — Vue and all sibling packages are never bundled.
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"]
}lib must include DOM and DOM.Iterable. paths are for IDE resolution during development only.
Type Bridge (src/types.ts)
The type layering pattern:
design-components-{name}/types → style props (CSS-level, framework-agnostic)
↓ extends
design-vue-{name}/types → Vue props (adds behaviour, events, slots)
↓ used by
Vibe{Name}.vue → defineProps<Vibe{Name}Props>()ts
// Re-export everything from the component-level types
export type { BadgeVariant, BadgeSize, BadgeStyleProps } from "@vibe-labs/design-components-badges/types";
export { BadgeVariants, BadgeSizes } from "@vibe-labs/design-components-badges/types";
// Vue-level props — extend the CSS-level props with behaviour
export interface VibeBadgeProps extends Omit<BadgeStyleProps, "dot"> {
label?: string;
bgColor?: string;
fgColor?: string;
autoColor?: boolean;
dismissible?: boolean;
}Vue props extend component-level style props, adding: behavioural props (dismissible, autoColor, loading), override props (bgColor, fgColor, color), content props (label, message, title), and Omit<> for CSS-only concerns.
Barrel (src/index.ts)
ts
/* ── Components ── */
export { default as VibeBadge } from "./components/VibeBadge.vue";
export { default as VibeBadgeCount } from "./components/VibeBadgeCount.vue";
/* ── Types ── */
export type { VibeBadgeProps, VibeBadgeCountProps } from "./types";
export type { BadgeVariant, BadgeSize, BadgeStyleProps } from "./types";
export { BadgeVariants, BadgeSizes } from "./types";
/* ── Composables ── */
// export { useXxx } from "./composables/useXxx";SFC Conventions
vue
<script setup lang="ts">
import { computed } from "vue";
import type { VibeBadgeProps } from "../types";
const props = withDefaults(defineProps<VibeBadgeProps>(), {
variant: "accent-subtle",
size: "md",
pill: true,
interactive: false,
dismissible: false,
});
const emit = defineEmits<{
dismiss: [];
}>();
defineOptions({ inheritAttrs: false });
</script>
<template>
<span
class="badge"
:data-variant="variant"
:data-size="size"
:data-pill="pill || undefined"
:data-interactive="interactive || undefined"
:data-removable="dismissible || undefined"
:role="interactive ? 'button' : undefined"
:tabindex="interactive ? 0 : undefined"
v-bind="$attrs"
>
<slot name="left" />
<span class="badge-label"><slot>{{ label }}</slot></span>
<slot name="right" />
</span>
</template>SFC Rules
Root element class = component-level base class (
badge,btn,card). All visual styling comes fromdesign-components-*.No
<style>blocks — ever. Only inline styles for dynamic overrides (e.g.colorOverride).Data attributes for variants/flags:
vue<!-- Enum variant — always bound --> :data-variant="variant" :data-size="size" <!-- Boolean flag — undefined removes the attribute entirely --> :data-pill="pill || undefined" :data-loading="loading || undefined" <!-- ARIA state — prefer native/ARIA over custom data attrs --> :aria-selected="selected" :aria-expanded="expanded" :disabled="disabled || undefined"Use
|| undefinedfor boolean flags —data-dot="false"would still match[data-dot]selectors.defineOptions({ inheritAttrs: false })+v-bind="$attrs"— explicit control over where attrs land.withDefaultsfor every optional prop. Defaults must match the documented component defaults.Events via
defineEmitswith typed tuple signatures.
Custom Color Override Pattern
For components supporting custom colours, locally override --color-accent:
ts
const customStyle = computed(() => {
if (!props.color) return undefined;
return {
"--color-accent": props.color,
"--color-accent-hover": `color-mix(in srgb, ${props.color} 85%, black)`,
"--color-accent-active": `color-mix(in srgb, ${props.color} 70%, black)`,
} as Record<string, string>;
});This cascades through all accent-derived variant tokens without per-variant overrides.
Composable Conventions
ts
// Return reactive refs and computed properties, not raw values
// Accept MaybeRefOrGetter<T> for flexibility
// Clean up side effects in onUnmounted / onScopeDispose
// Always prefix with "use"Common patterns: state management (useModal, useToast), DOM interaction (useClickOutside, useFocusTrap), data processing (useTableSort, useUploadQueue), input helpers (useInputField, useAutoResize), responsive (useContainerBreakpoint).
Provide/Inject for Compound Components
ts
// Parent provides context
const context = {
activeTab: ref("first"),
registerTab: (name: string) => { ... },
selectTab: (name: string) => { ... },
};
provide(TABS_INJECTION_KEY, context);
// Child injects — fail gracefully if used outside parent
const ctx = inject(TABS_INJECTION_KEY);Responsive Vue Components
The @vibe-labs/design-vue-responsive package wraps the container-query CSS components. One composable provides JS-level breakpoint observation:
vue
<VibeResponsiveContainer type="inline" name="main">
<slot />
</VibeResponsiveContainer>
<VibeResponsiveGrid :cols="1" :cols-md="2" :cols-lg="3">
<slot />
</VibeResponsiveGrid>
<VibeResponsiveStack breakpoint="md">
<slot />
</VibeResponsiveStack>useContainerBreakpoint(target) uses ResizeObserver to track the inline size of a container. Returns { current, above, below, width } — for conditional rendering that CSS container queries can't handle.
Accessibility Baseline
- Semantic HTML (
<button>,<input>,<nav>) not generic<div>with roles - ARIA attributes (
aria-label,aria-selected,aria-expanded,aria-disabled,role) - Keyboard navigation for all interactive components
- Focus management (modals trap focus, dropdowns restore focus)
- Screen reader announcements (
role="status"for live updates) - Use
data-*for visual-only modifiers, ARIA for state that assistive technology needs
Cross-Package Dependencies
| Type | Where declared | How it works |
|---|---|---|
| Token references | readme only | e.g. surfaces uses --color-neutral-* from colors |
| Component-level CSS | package.json deps in umbrella | @vibe-labs/design-components handles import order |
| Vue ↔ component types | package.json deps | @vibe-labs/design-components-{name} |
| Vue ↔ core utils | package.json deps | @vibe-labs/core |
| Vue ↔ Vue | rollup externals | never bundled, resolved at runtime |
Checklist
- Create
vibe-design-vue-{name}/directory - Add
package.json(copy template, update name + dependencies) - Add
tsconfig.json(copy template, updatepaths) - Add
vite.config.ts(copy template, update entry/name/fileName) - Create
src/types.ts— re-export component-level types + define Vue-level props - Create
src/components/Vibe{Name}.vue— SFC with<script setup>, typed props, no<style> - Create
src/index.tsbarrel - Add composables in
src/composables/if needed - Write
readme.md,developer.md,usage.md,contents.md - Run
npm run buildand verify dist - Add to umbrella
@vibe-labs/design-vueimports