Composables
Composables are the foundation of v0. They provide headless logic that you can use directly or through wrapper components.
Composables vs Components
Both approaches use the same underlying logic:
// Direct composable usage
const selection = createSelection({ multiple: true })
selection.register({ id: 'a', value: 'Apple' })
selection.select('a')<template>
<Selection.Root v-model="selected" multiple>
<Selection.Item value="Apple">Apple</Selection.Item>
</Selection.Root>
</template>When to Use Each
| Use Composables When | Use Components When |
|---|---|
| Need full control over rendering | Want declarative templates |
| Building custom abstractions | Standard UI patterns |
| Non-DOM contexts (stores, workers) | Accessibility attrs needed |
| Maximum flexibility | Faster development |
Components and composables are interchangeable. Every component uses a composable internally—you can always drop to the composable for more control.
Quick Reactivity Note
v0 uses minimal reactivity for performance. Unlike Vue’s default approach where everything is deeply reactive, v0 only makes reactive what must be reactive.
Selection state is always reactive — selectedId, selectedIds, and isSelected update your templates automatically. This covers 90% of use cases.
Registry collections are NOT reactive by default — methods like registry.values() return snapshots. If you need template updates when items change, wrap with useProxyRegistry():
// ❌ Won't update template when items change
const items = registry.values()
// ✅ Reactive — template updates automatically
const proxy = useProxyRegistry(registry)
// Use proxy.values in your templateThis is intentional! Most apps only need selection reactivity. For the full picture, see the Reactivity Guide.
Categories
Foundation
Factories that create other composables:
| Composable | Purpose |
|---|---|
| createContext | Type-safe provide/inject |
| createTrinity | [use, provide, context] tuple |
| createPlugin | Vue plugin factory |
Registration
Collection management primitives:
| Composable | Purpose |
|---|---|
| createRegistry | Base collection with lookup |
| createTokens | Design token aliases |
| createQueue | Time-based queue |
| createTimeline | Undo/redo history |
Selection
State management for selection patterns:
| Composable | Purpose |
|---|---|
| createSelection | Multi-select base |
| createSingle | Radio, tabs, accordion |
| createGroup | Checkboxes, tri-state |
| createStep | Wizard, stepper, carousel |
Forms
Form state and validation:
| Composable | Purpose |
|---|---|
| createForm | Validation, dirty tracking |
Reactivity
Reactive proxy utilities:
| Composable | Purpose |
|---|---|
| useProxyModel | v-model bridge |
| useProxyRegistry | Registry to reactive object |
Plugins
App-level features installed via app.use():
| Composable | Purpose |
|---|---|
| useTheme | Dark/light mode |
| useLocale | i18n, RTL |
| useBreakpoints | Responsive queries |
| useStorage | Persistent state |
Utilities
Standalone helpers:
| Composable | Purpose |
|---|---|
| createFilter | Array filtering |
| createPagination | Page navigation |
| createVirtual | Virtual scrolling |
Usage Patterns
Direct Factory Call
For standalone instances:
import { createSelection } from '@vuetify/v0'
const tabs = createSelection({ multiple: false })
tabs.register({ id: 'home', value: 'Home' })
tabs.register({ id: 'about', value: 'About' })
tabs.select('home')Context Injection
For component tree sharing:
// Parent
import { createSelectionContext } from '@vuetify/v0'
const [useTabSelection, provideTabSelection] = createSelectionContext({
namespace: 'tabs',
multiple: false,
})
provideTabSelection()
// Child
const selection = useTabSelection()
selection.select('home')Plugin Installation
For app-wide singletons:
import { createApp } from 'vue'
import { createThemePlugin } from '@vuetify/v0'
const app = createApp(App)
app.use(
createThemePlugin({
default: 'light',
themes: { light: {...}, dark: {...} },
}),
)Composing Composables
Build complex behavior by combining primitives:
import { createSelection, createFilter, createPagination } from '@vuetify/v0'
// Filterable, paginated selection
const items = ref([...])
const query = ref('')
const filter = createFilter()
const { items: filtered } = filter.apply(query, items)
const pagination = createPagination({
size: () => filtered.value.length,
itemsPerPage: 10,
})
const selection = createSelection({ multiple: true })
// Visible items with selection state
const visibleItems = computed(() => {
const start = pagination.pageStart.value
const end = pagination.pageStop.value
return filtered.value.slice(start, end)
})TypeScript
All composables are fully typed. The value type is inferred from registration:
interface MyItem {
id: string
label: string
}
const selection = createSelection()
selection.register({ id: '1', value: { id: '1', label: 'First' } as MyItem })
// Type-safe access via ticket
const ticket = selection.get('1')
ticket?.value // MyItemFrequently Asked Questions
Both are valid. Choose based on scope:
| Pattern | When to Use |
|---|---|
| Direct factory | Local state, single component, testing |
| Context injection | Shared state across component tree |
| Plugin installation | App-wide singletons |
// Direct - local to this component
const localSelection = createSelection()
// Context - shared with descendants
const [useSelection, provideSelection] = createSelectionContext()
provideSelection() // Children can now useSelection()Direct calls are simpler when you don’t need to share state. See Core for context patterns.
Yes. Three approaches depending on your needs:
Trinity’s third element — Built-in shared instance:
const [useTheme, provideTheme, theme] = createThemeContext()
// 'theme' is the shared default instance
// Access it anywhere without injection
theme.current.value // Works outside components, in tests, etc.Module singleton — Export a factory result:
export const globalSelection = createSelection({ multiple: true })Plugin installation — App-wide via dependency injection:
app.use(createSelectionPlugin({ multiple: true }))Trinity’s third element is the idiomatic v0 approach—see The Trinity Pattern for details. Module singletons work outside Vue. Plugins integrate with devtools.
Composables follow Vue’s reactivity lifecycle:
Created — Factory call allocates refs and state
Active — Reactive updates propagate normally
Cleanup — When the creating component unmounts,
onUnmountedhooks run
const selection = createSelection()
// Registered items persist until explicitly unregistered
selection.register({ id: 'a', value: 1 })
onUnmounted(() => {
// Manual cleanup if needed
selection.unregister('a')
})For context-provided composables, the instance lives as long as the providing component. Child components that inject don’t affect the lifecycle.
Minimal. Plugin installation runs once at app startup:
The injection is a single Map lookup, same as any inject() call. There’s no per-render overhead—you’re accessing the same object reference.
Yes. Composables are designed for composition:
import {
createSelection,
createFilter,
createPagination,
} from '@vuetify/v0'
// Each composable manages its own state
const selection = createSelection({ multiple: true })
const filter = createFilter()
const pagination = createPagination({ itemsPerPage: 10 })
// Wire them together
const filtered = filter.apply(query, items)
const paginated = computed(() =>
filtered.value.slice(pagination.pageStart.value, pagination.pageStop.value)
)Each composable is independent. They don’t interfere with each other unless you explicitly connect them. See Composing Composables above for patterns.
Composables using Vue’s reactivity show up in DevTools automatically. For better debugging:
Named refs — Use
ref()with descriptive variable namesCustom inspector — Plugins registered via
app.use()appear in DevToolsConsole logging — Refs are reactive, use
toRaw()for snapshots
import { toRaw } from 'vue'
const selection = createSelection()
console.log(toRaw(selection.selectedIds)) // Plain Set, not reactive proxyFor registry-based composables, enable events: true to trace registration changes via useProxyRegistry.