Appearance
Build Guide - Vue Component Packages ​
How to create a new @carrot/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 ​
carrot-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/
│ ├── Carrot{Name}.vue # primary component
│ └── Carrot{Sub}.vue # sub-components
└── composables/ # optional - headless logic
└── use{Feature}.tspackage.json ​
json
{
"name": "@carrot/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": {
"@carrot/core": "*",
"@carrot/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
@carrot/*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: "CarrotDesignVue{Name}",
formats: ["es"],
fileName: "index",
},
rollupOptions: {
external: ["vue", /^@carrot\//],
output: { globals: { vue: "Vue" } },
},
},
plugins: [vue(), dts({ insertTypesEntry: true, copyDtsFiles: true })],
});Critical: external: ["vue", /^@carrot\//] - 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": {
"@carrot/core": ["../../../carrot-core/src"],
"@carrot/design-components-{name}/types": ["../../components/carrot-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
Carrot{Name}.vue → defineProps<Carrot{Name}Props>()ts
// Re-export everything from the component-level types
export type { BadgeVariant, BadgeSize, BadgeStyleProps } from "@carrot/design-components-badges/types";
export { BadgeVariants, BadgeSizes } from "@carrot/design-components-badges/types";
// Vue-level props - extend the CSS-level props with behaviour
export interface BadgeProps 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 CarrotBadge } from "./components/CarrotBadge.vue";
export { default as CarrotBadgeCount } from "./components/CarrotBadgeCount.vue";
/* ── Types ── */
export type { BadgeProps, BadgeCountProps } 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 { BadgeProps } from "../types";
const props = withDefaults(defineProps<BadgeProps>(), {
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 ​
@carrot/design-vue-layout ships container-query primitives alongside the rest of the layout vocabulary. One composable provides JS-level breakpoint observation:
vue
<CarrotContainer type="inline" name="main">
<slot />
</CarrotContainer>
<CarrotGrid :cols="1" :cols-md="2" :cols-lg="3">
<slot />
</CarrotGrid>
<CarrotSwitcher breakpoint="md">
<slot />
</CarrotSwitcher>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 | @carrot/design-components handles import order |
| Vue ↔ component types | package.json deps | @carrot/design-components-{name} |
| Vue ↔ core utils | package.json deps | @carrot/core |
| Vue ↔ Vue | rollup externals | never bundled, resolved at runtime |
Checklist ​
- Create
carrot-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/Carrot{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
@carrot/design-vueimports