Building Frameworks
When building a component framework, you’re often reimplementing the same patterns: selection state, keyboard navigation, form validation, and focus management. v0 provides these behaviors as headless primitives so you can focus on what makes your framework unique—its design language.
Prerequisites This guide builds on concepts from Core, Components, and Composables. Read those first if you’re new to v0.
Getting Started
New Project
Use vuetify0 create to scaffold a new project with v0 pre-configured:
pnpm create vuetify0npm create vuetify0yarn create vuetify0bun create vuetify0Existing Project
Add v0 to an existing Vue project:
pnpm add @vuetify/v0npm install @vuetify/v0yarn add @vuetify/v0bun add @vuetify/v0v0 uses subpath exports↗︎ for tree-shaking:
// Import everything
import { createSingle, Atom } from '@vuetify/v0'
// Or import specific modules
import { createSingle } from '@vuetify/v0/composables'
import { Atom } from '@vuetify/v0/components'
import type { ID } from '@vuetify/v0/types'
import { isObject } from '@vuetify/v0/utilities'
import { IN_BROWSER } from '@vuetify/v0/constants'Two Integration Patterns
This guide covers two approaches:
| Pattern | When to Use |
|---|---|
| Pattern A: Behavior-Focused | Need complex state (selection, navigation, validation) with full rendering control |
| Pattern B: Component Wrappers | Building styled components on top of v0’s headless primitives |
v0’s composables are completely headless—they manage state and behavior without any DOM assumptions. This makes them ideal for building design systems that need complete control over markup and styling.
Core Concepts
Direct vs Context APIs
v0 composables offer two API surfaces. The direct API creates standalone instances—perfect for component-local state. The context API uses Vue’s provide/inject↗︎ for sharing state across component trees.
// Direct instance (component-local)
const tabs = createSingle({ mandatory: true })
// Context trinity (for dependency injection)
const [useTabs, provideTabs, defaultTabs] = createSingleContext()Start with the direct API. Only reach for contexts when you need to share state between parent and child components that can’t communicate via props.
The Ticket System
When you register items with a v0 registry, you get back “tickets”—plain objects that contain reactive properties and bound methods. Properties like isSelected are Vue refs that update automatically when selection state changes.
tabs.register({ id: 'home', value: 'Home' })
const ticket = tabs.get('home')
ticket.id // 'home' (static)
ticket.value // 'Home' (static)
ticket.index // 0 (static, position in registry)
ticket.isSelected // Ref<boolean> - reactive!
ticket.isSelected.value // true/false - access the value
ticket.toggle() // Toggle selection
ticket.select() // Select this itemTemplate Iteration
Registry internals aren’t directly exposed for template iteration. useProxyRegistry wraps a registry and provides reactive arrays that update when items change:
<script setup lang="ts">
import { createSingle, useProxyRegistry } from '@vuetify/v0/composables'
const tabs = createSingle({ mandatory: 'force', events: true })
const proxy = useProxyRegistry(tabs)
// proxy.values is a reactive array that updates when registry changes
// proxy.keys and proxy.entries also available
</script>
<template>
<button v-for="tab in proxy.values" :key="tab.id" @click="tab.toggle">
{{ tab.value }}
</button>
</template> The events: true option is required. useProxyRegistry listens for registry events to invalidate its cache.
Pattern A: Behavior-Focused
Use v0’s composables directly when you need complex state management—selection, navigation, validation—but want full control over rendering.
Adding Keyboard Navigation
v0 composables handle state; you add the interaction layer:
Multi-Selection with Groups
Use createGroup for checkbox-style multi-selection with tri-state support:
Pattern B: Component Wrappers
Wrap v0’s headless components with your design system’s styling. v0 handles behavior, accessibility, and keyboard navigation—you control the visual presentation.
Polymorphic Elements
The Atom component provides polymorphic rendering via the as prop—render as any HTML element while keeping your component’s API consistent:
The example uses BEM naming↗︎—a convention for organizing CSS in component libraries. Blocks (.my-button), elements (__icon), and modifiers (--filled, --primary) create predictable, collision-free class names.
Styling Headless Components
Wrap v0’s compound components with custom CSS. The components expose data attributes like data-state for styling different states:
Form Components
v0’s form components handle focus, keyboard interaction, and ARIA attributes. Apply your styles via classes:
Plugin Architecture
v0’s plugins follow Vue’s plugin pattern↗︎ with additional structure for namespaced context provision.
v0 plugins are designed to be order-independent. Each plugin gracefully handles missing dependencies by providing sensible fallbacks.
Creating Custom Plugins
Use createPluginContext to build your own plugins without boilerplate. It returns the standard [createContext, createPlugin, useContext] triple:
import { createPluginContext } from '@vuetify/v0'
interface ThemeOptions {
namespace?: string
primary?: string
}
interface ThemeContext {
primary: string
setPrimary: (color: string) => void
}
function createThemeFeature (options: Omit<ThemeOptions, 'namespace'>): ThemeContext {
const primary = shallowRef(options.primary ?? '#1976d2')
return { primary, setPrimary: (color) => { primary.value = color } }
}
export const [createMyThemeContext, createMyThemePlugin, useMyTheme] =
createPluginContext<ThemeOptions, ThemeContext>(
'my-ui:theme',
options => createThemeFeature(options),
)
// In main.ts
app.use(createMyThemePlugin({ primary: '#e91e63' }))
// In any component
const { primary, setPrimary } = useMyTheme()See createPlugin for the full API including persist/restore lifecycle hooks for saving and rehydrating plugin state.
SSR Safety
v0 is designed for universal rendering. Use the provided constants and composables to guard browser-only code:
import { useHydration, useWindowEventListener } from '@vuetify/v0'
// SSR-safe event listener (no-op on server, auto-cleanup)
useWindowEventListener('resize', handler)
// Reactive hydration state for conditional rendering
const { isHydrated } = useHydration()
// In templates: v-if="isHydrated"The isHydrated shallowRef is false during SSR and becomes true after the root component mounts. This prevents hydration mismatches when rendering browser-dependent content.
TypeScript Patterns
v0 uses generics extensively. When extending composables, provide your custom ticket and context types:
Vue’s shallowReactive↗︎ and computed↗︎ are used internally—understanding these helps when debugging reactivity issues.
Complete Example: @example/my-ui
To see everything come together, we’ve included a complete example library that demonstrates the patterns in this guide.
The example package includes:
my-ui/
├── package.json # Peer deps on @vuetify/v0 and vue
├── vite.config.ts # Library build config
├── src/
│ ├── index.ts # Public exports
│ ├── plugin.ts # Vue plugin with v0 setup
│ └── components/
│ ├── MyButton.vue # Atom wrapper (polymorphic)
│ ├── MyTabs.vue # createSingle + keyboard nav
│ └── MyAccordion.vue # ExpansionPanel wrapperView the full source in the examples directory↗︎.