Skip to main content
Vuetify0 is now in alpha!
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

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.


AdvancedApr 5, 2026
Info

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:

bash
pnpm create vuetify0

Existing Project

Add v0 to an existing Vue project:

bash
pnpm add @vuetify/v0

v0 uses subpath exports↗ for tree-shaking:

ts
// 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:

PatternWhen to Use
Pattern A: Behavior-FocusedNeed complex state (selection, navigation, validation) with full rendering control
Pattern B: Component WrappersBuilding styled components on top of v0’s headless primitives
Tip

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.

ts
// Direct instance (component-local)
const tabs = createSingle({ mandatory: true })

// Context trinity (for dependency injection)
const [useTabs, provideTabs, defaultTabs] = createSingleContext()
Tip

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.

ts
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 item

Template Iteration

Registry internals aren’t directly exposed for template iteration. useProxyRegistry wraps a registry and provides reactive arrays that update when items change:

vue
<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>
Tip

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.

Behavior-Focused Tabs

Custom tabs using createSingle and useProxyRegistry with ARIA attributes and accessible keyboard navigation.

Profile

Content for the profile tab.

Adding Keyboard Navigation

v0 composables handle state; you add the interaction layer:

Keyboard Navigation

Arrow key, Home, and End key support added to the tabs above.

Overview

This panel demonstrates keyboard navigation. Use arrow keys to move between tabs, Home/End to jump to first/last.

Navigate HomeEnd Jump

Multi-Selection with Groups

Use createGroup for checkbox-style multi-selection with tri-state support:

Multi-Select Accordion

Accordion with expand/collapse-all using createGroup and tri-state support.

0 of 3 expanded
Ask AI
How does this compare to v0's built-in Tabs component?

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:

Polymorphic Button

An Atom-based button with BEM class names and style/size/color variants.

MyButton.vue

Link Button

Variants

Sizes

Colors

Tip

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:

Styled ExpansionPanel

ExpansionPanel wrapped with custom CSS targeting data attributes and slot props for visual state.

Wrapping v0's ExpansionPanel with custom styling:

Headless components, composables, and utilities for building design systems. Full TypeScript support with generic constraints.

Form Components

v0’s form components handle focus, keyboard interaction, and ARIA attributes. Apply your styles via classes:

Styled Checkbox

Checkbox wrapped with label and description, styled via data attributes for checked and disabled states.

Wrapping v0's Checkbox with custom styling:

Selected: notifications, updates

Plugin Architecture

v0’s plugins follow Vue’s plugin pattern↗ with additional structure for namespaced context provision.

Plugin Access

Reading and toggling theme and breakpoint state anywhere in the component tree via useTheme and useBreakpoints.

Access v0 plugins anywhere in your component tree:

useTheme()

Current:dark
Dark mode:true

useBreakpoints()

Current:xs
Width:0px
xssmmdlgxl

// plugins/index.ts
app.use(createThemePlugin({ ... }))
app.use(createBreakpointsPlugin({ ... }))

Tip

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:

ts
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:

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

SSR Hydration Guard

Using useHydration() and IN_BROWSER to safely defer browser-only rendering until after hydration.

v0 provides two ways to handle SSR safely:

IN_BROWSER constant

Static check, evaluated once at import time.

Server
if (IN_BROWSER) {
  // Safe to use window, document
}

useHydration()

Reactive ref, changes after mount.

SSR
const { isHydrated } = useHydration()
// v-if="isHydrated"

Browser-only content

Loading browser data...

TypeScript Patterns

v0 uses generics extensively. When extending composables, provide your custom ticket and context types:

Type Extension

Extending SelectionTicket and SelectionContext with custom properties for type-safe file selection.

Extend v0's ticket and context types for custom properties:

interface FileTicket extends SelectionTicket<string> { icon, size }
0 selected 0 B

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.

Complete Design System

MyButton, MyTabs, and MyAccordion assembled from v0 primitives — all patterns from this guide working together.

Example library built on v0 — see my-ui/ source

MyButton

Link

MyTabs

Overview panel content. The tabs handle keyboard navigation automatically.

MyAccordion

Install the package and register the plugin in your Vue app.

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 wrapper

View the full source in the examples directory↗.

Was this page helpful?

© 2016-1970 Vuetify, LLC
Ctrl+/