
Vuetify0
- 610 installs
- 825 repo stars
- Updated August 5, 2026
- vuetifyjs/0
vuetify0 is a Vuetify headless agent skill that provides Vue 3 composable logic for selection, validation, virtual scrolling, and WAI-ARIA components without shipping any CSS styling.
About
vuetify0 is the official Vuetify headless primitives skill for Vue 3 projects that need complex interactive behavior without reinventing state machines or DOM observers. Logic-only composables cover single, multi, grouped, nested, and stepped selection; form validation; registries and context providers; virtual scrolling; pagination; data tables; focus traversal; keyboard shortcuts; resize, intersection, and mutation observers; popovers; snackbars; SSR-safe browser checks; theming; RTL; and breakpoints. The skill triggers on @vuetify/v0, vuetify0, v0, createX composables, or imports starting with #v0, and feeds upcoming Vuetify 4 through minor releases. Developers reach for vuetify0 when building accessible compound components, headless design-system primitives, or data-heavy tables where hand-rolled selection and observer code is error-prone. Styling remains entirely in the application layer while WAI-ARIA and keyboard contracts stay consistent with Vuetify’s roadmap.
- 40 components, 70 composables, and 24 utilities across 15 categories
- Headless primitives for selection state (single, multi, grouped, nested, stepped)
- Built-in support for form validation, virtual scrolling, pagination, data tables, and WAI-ARIA compound components
- Tree-shakeable imports with no global plugin required
- Decision table provided to match problems to the correct createX composable before writing custom logic
Vuetify0 by the numbers
- 610 all-time installs (skills.sh)
- +10 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #561 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vuetifyjs/0 --skill vuetify0Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 610 |
|---|---|
| repo stars | ★ 825 |
| Last updated | August 5, 2026 |
| Repository | vuetifyjs/0 ↗ |
How do you build headless Vue 3 interactive components?
Get battle-tested headless Vue 3 logic for complex interactive components without writing selection, validation, or observer code from scratch.
Who is it for?
Vue 3 developers building accessible data tables, multi-select, or compound widgets who want Vuetify-aligned headless logic without Vuetify CSS.
Skip if: Projects needing pre-styled Vuetify components today or frameworks other than Vue 3.
When should I use this skill?
Trigger on @vuetify/v0, vuetify0, v0, createX composables, or #v0 import paths for interactive UI logic.
What you get
Headless Vue 3 composables for selection, validation, observers, and accessible compound component behavior with zero bundled styles.
- headless composable integrations
- accessible compound components
Files
Vuetify0
Headless Vue 3 primitives. Logic only, zero styling. Feeds Vuetify 4 through minor releases.
<!-- @generated:counts --> <!-- Generated by scripts/generate-skill.ts — do not edit by hand. --> 40 components, 70 composables, 24 utilities across 15 categories. Compound-component APIs with WAI-ARIA semantics. <!-- @generated:counts:end -->
Install
pnpm install @vuetify/v0No global plugin required. Tree-shakeable imports:
import { createSelection, useBreakpoints } from '@vuetify/v0'
import { Tabs, Dialog } from '@vuetify/v0/components'Decision table — reach for these first
Check this table before writing custom logic. Match by problem, not by keyword.
| Problem | Use | Category |
|---|---|---|
| Single-choice state (tabs, theme picker) | createSingle | selection |
| Multi-choice state (filters, tag pickers) | createSelection | selection |
| Select-all with tri-state | createGroup | selection |
| Tree / nested selection (treeview, menus) | createNested | selection |
| Wizard / carousel step tracking | createStep | selection |
| Id-based value store (shared across sub-components) | createModel | selection |
| Form with async validation + dirty tracking | createForm + createValidation | forms |
| Slider / range / knob state | createSlider | forms |
| Autocomplete / combobox | createCombobox | forms |
| Spin-button numeric input | createNumberField / createNumeric | forms |
| One-time-password / verification-code value | createOtp | forms |
| Paginated or virtualized list | createPagination, createVirtual | data |
| Sortable / filterable table | createDataTable, createFilter | data |
| Breadcrumb trail derived from route | createBreadcrumbs | utilities |
| Overflow / responsive menu (hides overflowing items) | createOverflow | utilities |
| Type-safe provide/inject | createContext | foundation |
| Reactive registry of ids → values | createRegistry | registration |
| Auto-dismissing queue (snackbars, toasts) | createQueue | registration |
| Scheduled events over time (timeline, animation) | createTimeline | registration |
| Design-token graph (theme, spacing scales) | createTokens | registration |
| Floating UI positioning (popover, tooltip, menu) | usePopover | system |
| Enter/leave animation orchestration | usePresence | system |
| Roving tabindex (list, menubar) | useRovingFocus | system |
| Virtual focus (combobox listbox) | useVirtualFocus | system |
| Click outside / keyboard shortcut / event listener | useClickOutside, useHotkey, useEventListener | system |
| ResizeObserver / IntersectionObserver / MutationObserver | useResizeObserver, etc. | system |
| rAF loop or setTimeout with pause/resume | useRaf, useTimer | system |
| Responsive breakpoints | useBreakpoints, useMediaQuery | plugins |
| Localized strings + date/number format | useLocale, useDate | plugins |
| Theme (light/dark/custom palette) | useTheme | plugins |
| RTL direction awareness | useRtl | plugins |
| z-index stacking for overlays | useStack | plugins |
| Notifications / snackbar queue plugin | useNotifications | plugins |
| Feature flags / permission checks | useFeatures, usePermissions | plugins |
| Persisted state (localStorage / sessionStorage) | useStorage | plugins |
| Structured logging with adapters | useLogger | plugins |
| SSR-safe mount detection | useHydration | plugins |
Full API and type signatures: see references/REFERENCE.md.
<!-- @generated:api --> <!-- Generated by scripts/generate-skill.ts — do not edit by hand. -->
Full API surface
Every public composable and utility, grouped by category. (stable) and (draft) are marked; unmarked entries are preview.
Composables
Foundation:
createContext— Factory for creating type-safe Vue dependency injection contexts. (stable)createPlugin— Factory for creating Vue plugins with proper context provision. (stable)createTrinity— Factory for creating the trinity pattern tuple used throughout the codebase. (stable)
Registration:
createQueue— A queue composable for managing time-based collectionscreateRegistry— A foundational composable for managing collections of items (tickets)createTimeline— Bounded undo/redo system with overflow management.createTokens— Design token registry with alias resolution and W3C Design Tokens format support.
Forms:
createCombobox— Orchestrator composable that coordinates selection, popover, and virtual focus for a combobox (autocomplete/typeahead) pattern.createForm— Form composable that coordinates validation across multiple fields.createInput— Shared form field primitive.createNumberField— Orchestrator composable that composes createInput + createNumeric + Intl.NumberFormat.createNumeric— Pure numeric math primitive.createOtp— Headless state for a fixed-length one-time-password / verification-code value.createRating— Lightweight rating composable for bounded discrete values.createSlider— Composable for managing slider state: value math, step snapping, percentage conversion, and multi-thumb value operations.createValidation— Per-input validation composable built on createGroup.
Selection:
createGroup— Multi-selection composable that extends createSelection with batch operations and tri-state support.createModel— Recently redesigned from selection systemcreateNested— Hierarchical tree management composable extending createGroupcreateSelection— Selection composable that extends createModel with multi-select, mandatory enforcement, auto-enrollment, and ticket self-methods.createSingle— Single-selection composable that extends createSelection to enforce only one selected item.createStep— Navigation composable that extends createSingle with first/last/next/prev/step methods.
Data:
createDataGrid— Main factory that wires together column layout, cell editing, row ordering, and row spanning on top of a createDataTable pipeline.createDataTable— Composable data table that composes existing v0 primitives rather than reimplementing their logic.createFilter— Reactive array filtering composable with multiple filter modes.createKanban— Headless data-flow management for two-level sortable boards.createPagination— Lightweight pagination composable for navigating through pages.createSortable— Headless ordered-list primitive.createVirtual— Virtual scrolling composable for efficiently rendering large lists.
Semantic:
createBreadcrumbs— Breadcrumb navigation composable built on createSingle.createOverflow— Composable for computing how many items fit in a container based on available width.createProgress— Progress composable built on createModel for segment tracking.
Plugins:
useBreakpoints— Responsive breakpoint detection composable with window resize handling.useDate— Date manipulation composable with adapter pattern for date operations.useFeatures— Feature flag system with boolean and token-based features.useHydration— SSR hydration state management composable.useLocale— Internationalization (i18n) composable with adapter pattern for message translation.useLogger— Logging composable with adapter pattern supporting console, consola, and pino.useNotifications— Notification management composable built on createRegistry and createQueue.usePermissions— Permission management composable with support for RBAC and ABAC patterns.useRtl— RTL (right-to-left) direction composable with adapter pattern.useRules— Validation rule composable with Standard Schema support.useStack— Overlay z-index stacking composableuseStorage— Reactive storage composable with adapter pattern for localStorage, sessionStorage, or memory.useTheme— Theme management composable with token resolution and CSS variable injection.useTooltip— Region-scoped tooltip coordination plugin.
System:
useClickOutside— Detects clicks outside of specified element(s) with automatic cleanup.useDelay— Schedule open and close transitions with configurable delays.useDragDrop— Headless drag-and-drop primitive.useEventListener— Event listener composable with automatic cleanup on scope disposal.useHotkey— Hotkey listener composable with key combination and sequence support.useImage— Image loading state machine with deferred loading support.useIntersectionObserver— IntersectionObserver composable with lifecycle management. (stable)useLazy— Deferred content rendering composable for performance optimization.useMediaQuery— Reactive media query composable with automatic cleanup.useMutationObserver— MutationObserver composable with lifecycle management. (stable)usePopover— Composable for native popover API behavior with CSS anchor positioning.usePresence— Animation-agnostic mount lifecycle composable.useRaf— Scope-disposed safe requestAnimationFrame composable.useResizeObserver— ResizeObserver composable with lifecycle management. (stable)useRovingFocus— Roving tabindex composable for keyboard navigation within composite widgets.useTimer— A reactive timer composable with pause/resume support.useToggleScope— Conditionally manages an effect scope based on a reactive boolean condition.useVirtualFocus— Virtual focus composable for aria-activedescendant keyboard navigation.
Reactivity:
useProxyModel— Proxy composable for bidirectional sync between a model context and v-model.useProxyRegistry— Proxy composable for reactive registry keys, values, entries, and size.
Transformers:
toArray— Utility function to normalize single values and arrays into arrays.toElement— Resolves various element reference types to a DOM Element.toHighlight— Pure transformer — no DOM, no state, no registry, no reactivity.toReactive— Utility function to convert values and refs into reactive proxies with ref unwrapping.
Utilities
Utilities:
apcaclamp— Clamps a value between a minimum and maximum (stable)foregroundhexToRgbisArray— Checks if a value is an array (stable)isBoolean— Checks if a value is a boolean (stable)isElement— Checks if a value is a DOM Element (stable)isFunction— Checks if a value is a function (stable)isNaN— Checks if a value is NaN (Not a Number) (stable)isNull— Checks if a value is null (stable)isNullOrUndefined— Checks if a value is null or undefined (stable)isNumber— Checks if a value is a number (stable)isObject— Checks if a value is a plain object (excludes null and arrays) (stable)isPrimitive— Checks if a value is a primitive (string, number, or boolean) (stable)isString— Checks if a value is a string (stable)isSymbol— Checks if a value is a symbol (stable)isThenable— Checks if a value is a thenable (any object with a.thenmethod).isUndefined— Checks if a value is undefined (stable)isV0Error— Type guard for v0-thrown errors.mergeDeep— Deeply merges source objects into a target object, returning a new object (stable)range— Creates an array of sequential numbers (stable)rgbToHexuseId— Generates a unique ID, using Vue's useId when in component context (stable)V0Error— Structured error thrown by v0 internals.
<!-- @generated:api:end -->
Compound-component pattern
All components are headless and compound. Root owns state, children are named sub-components.
<script setup lang="ts">
import { Tabs } from '@vuetify/v0/components'
import { shallowRef } from 'vue'
const active = shallowRef('overview')
</script>
<template>
<Tabs.Root v-model="active">
<Tabs.List>
<Tabs.Item value="overview">Overview</Tabs.Item>
<Tabs.Item value="details">Details</Tabs.Item>
</Tabs.List>
<Tabs.Panel value="overview">Overview content</Tabs.Panel>
<Tabs.Panel value="details">Details content</Tabs.Panel>
</Tabs.Root>
</template><!-- @generated:components --> <!-- Generated by scripts/generate-skill.ts — do not edit by hand. --> Available components:
- Primitives:
AspectRatio,Atom,Portal,Presence - Providers:
Group,Locale,Scrim,Selection,Single,Step,Theme - Actions:
Button,Toggle - Forms:
Checkbox,Combobox,Form,Input,NumberField,Radio,Rating,Select,Slider,Switch - Disclosure:
AlertDialog,Collapsible,Dialog,ExpansionPanel,Popover,Tabs,Tooltip,Treeview - Semantic:
Avatar,Breadcrumbs,Carousel,Image,Overflow,Pagination,Progress,Snackbar,Splitter
Planned (not yet exported — do not import):
Alert,DataGrid,DatePicker,DateRangePicker,Kanban,Otp,TimePicker,Tour,Virtualizer
<!-- @generated:components:end -->
More compound examples: see references/component-examples.md.
Must-read rules
1. Check utilities before writing helpers
Import from #v0/utilities (internal) or @vuetify/v0 (external):
- Type guards:
isFunction,isString,isNumber,isBoolean,isObject,isArray,isNull,isUndefined,isNullOrUndefined,isPrimitive,isSymbol,isNaN,isElement - Data:
mergeDeep,clamp,range,useId
2. Check globals before SSR branches
Import from #v0/constants/globals:
IN_BROWSER— replacestypeof window !== 'undefined'SUPPORTS_TOUCH,SUPPORTS_MATCH_MEDIA,SUPPORTS_OBSERVER,SUPPORTS_INTERSECTION_OBSERVER,SUPPORTS_MUTATION_OBSERVER
3. Reactivity defaults
shallowReffor primitivesreffor objects/arraystoReffor derived values (default)computedonly when caching expensive work
4. Compound components only
Sub-components talk to the root via createContext. Never prop-drill state between siblings. Never build a monolithic component when the root + sub-component pattern fits.
Detailed anti-patterns: see references/anti-patterns.md.
Paper and Vuetify relationship
@vuetify/v0— headless (this skill)@vuetify/paper— styling primitives that depend on v0vuetifyv4 — Material Design framework, integrates v0 via minor releases
When the user asks to "style" a v0 component or build a design system, point them at @vuetify/paper or a Paper-based design system (e.g., Emerald, Helix). Keep v0 itself headless.
Vuetify MCP
For live API schemas, guides, and release notes, prefer the Vuetify MCP server over guessing:
claude mcp add vuetify-mcp https://mcp.vuetifyjs.com/mcpUseful tools (fully qualified names required):
vuetify-mcp:get_vuetify0_component_list— all components with categoriesvuetify-mcp:get_vuetify0_composable_list— all composables with categoriesvuetify-mcp:get_vuetify0_component_guide— guide for a named componentvuetify-mcp:get_vuetify0_composable_guide— guide for a named composablevuetify-mcp:get_vuetify0_installation_guide— installation + bootstrap
Resources
- Live docs: https://0.vuetifyjs.com
- API reference: references/REFERENCE.md
- Selection patterns (single, multi, group, nested, step): references/selection-patterns.md
- Component compound patterns: references/component-examples.md
- Anti-patterns and migrations: references/anti-patterns.md
- Layer decisions (component vs composable vs both): references/layer-decisions.md
- Authoring guide (build a new compound component): references/authoring-guide.md
Anti-Patterns: What NOT to Do
Common mistakes when using v0 and their proper solutions.
Contents
- Selection — reach for
createSelection/createSingle/createGroupbefore custom arrays - Context — use
createContextinstead of bareprovide/inject - Form — use
createForm+createValidationinstead of manual refs - Browser — use
IN_BROWSERand support constants instead oftypeof window - Registry — use
createRegistryinstead of hand-rolled id maps - Performance —
shallowRefprimitives,toRefderivations,computedonly for cached work - Component — build compound roots + sub-components, never monoliths
- Migration Strategy — phased rewrites from manual to v0 composables
- Quick Fixes — common one-liner corrections
❌ Manual Selection Logic
// BAD - Custom selection state
const selected = ref<string[]>([])
const isSelected = (id: string) => selected.value.includes(id)
const toggle = (id: string) => {
const index = selected.value.indexOf(id)
if (index >= 0) {
selected.value.splice(index, 1)
} else {
selected.value.push(id)
}
}
// Problems:
// - No reactivity optimization
// - No mandatory selection support
// - No proper event handling
// - Manual array manipulation bugs
// - No tri-state support for groups// GOOD - Use v0 selection composables
const selection = createSelection({ multiple: true })
selection.toggle(id) // Handles all the complexity❌ Custom Single Selection
// BAD - Manual single selection
const active = ref<string | null>(null)
const select = (id: string) => {
active.value = active.value === id ? null : id
}
// Problems:
// - No mandatory selection support
// - Manual deselection logic
// - No registration system
// - No proper change events// GOOD - Use createSingle
const single = createSingle({ mandatory: 'force' })
single.select(id) // Handles mandatory logic❌ Array-Based Group Selection
// BAD - Manual tri-state logic
const selected = ref<string[]>([])
const allIds = computed(() => items.value.map(item => item.id))
const isAllSelected = computed(() =>
selected.value.length === allIds.value.length
)
const isSomeSelected = computed(() =>
selected.value.length > 0 && selected.value.length < allIds.value.length
)
const toggleAll = () => {
if (isAllSelected.value) {
selected.value = []
} else {
selected.value = [...allIds.value]
}
}
// Problems:
// - Complex tri-state logic
// - Manual "select all" implementation
// - No proper indeterminate state
// - Performance issues with large lists// GOOD - Use createGroup
const group = createGroup()
group.toggleAll() // Perfect tri-state handlingContext Anti-Patterns
❌ Manual Provide/Inject
// BAD - Unsafe provide/inject
provide('theme', themeState)
// In child component
const theme = inject('theme') // Could be undefined!
// Problems:
// - No type safety
// - Silent failures if provider missing
// - No helpful error messages
// - Hard to debug injection chains// GOOD - Type-safe context
const [useTheme, provideTheme] = createContext<ThemeState>('Theme')
// Provider
provideTheme(themeState)
// Consumer - throws descriptive error if missing
const theme = useTheme() // Always defined, type-safe❌ Global State Instead of Context
// BAD - Global singletons
const globalTheme = reactive({ mode: 'light' })
// Problems:
// - No isolation between app instances
// - Breaks SSR
// - Hard to test
// - No proper provider hierarchy// GOOD - Proper context hierarchy
const [useTheme, provideTheme] = createContext<Theme>('Theme')
// Each provider creates isolated scopeForm Anti-Patterns
❌ Manual Form Validation
// BAD - Custom validation logic
const email = ref('')
const emailError = ref('')
const validateEmail = () => {
if (!email.value) {
emailError.value = 'Required'
return false
}
if (!email.value.includes('@')) {
emailError.value = 'Invalid email'
return false
}
emailError.value = ''
return true
}
// Problems:
// - Manual error state management
// - No async validation support
// - No field registration
// - No form-level validation
// - Repetitive validation logic// GOOD - Use createForm
const form = createForm()
form.register({
id: 'email',
value: '',
rules: [
v => !!v || 'Required',
v => v.includes('@') || 'Invalid email',
async v => await checkAvailable(v) || 'Email taken'
]
})Browser Anti-Patterns
❌ Manual SSR Checks
// BAD - Unsafe environment detection
if (typeof window !== 'undefined') {
// Browser-only code
}
// Problems:
// - Not tree-shakeable
// - Verbose
// - Easy to get wrong
// - No hydration considerations// GOOD - Use provided constants
import { IN_BROWSER } from '@vuetify/v0/constants'
if (IN_BROWSER) {
// Browser-only code
}❌ Manual DOM Observers
// BAD - Manual ResizeObserver
const observer = new ResizeObserver(entries => {
// Handle resize
})
onMounted(() => {
if (elementRef.value) {
observer.observe(elementRef.value)
}
})
onUnmounted(() => {
observer.disconnect()
})
// Problems:
// - Manual lifecycle management
// - Memory leaks if cleanup forgotten
// - No SSR safety
// - Repetitive setup code// GOOD - Use composable
const { width, height } = useResizeObserver(elementRef)
// Auto-cleanup on unmount❌ Manual Event Listeners
// BAD - Manual event management
const handleResize = () => {
// Handle resize
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
// Problems:
// - Manual cleanup required
// - Memory leaks possible
// - No passive/capture options
// - Repetitive lifecycle code// GOOD - Auto-cleanup events
useEventListener(window, 'resize', handleResize)
// Automatically removed on unmountRegistry Anti-Patterns
❌ Manual Collection Management
// BAD - Manual item tracking
const items = ref(new Map<string, Item>())
const register = (item: Item) => {
items.value.set(item.id, item)
}
const unregister = (id: string) => {
items.value.delete(id)
}
// Problems:
// - Manual lifecycle management
// - No automatic cleanup
// - No lifecycle events
// - No type safety// GOOD - Use registry
const registry = createRegistry<Item>()
// Auto-cleanup on component unmount
registry.register({ id: 'item1', value: item })Performance Anti-Patterns
❌ Recreating Composables
// BAD - Creating composables inside computed/watch
const count = computed(() => {
const selection = createSelection() // New instance every time!
return selection.selected.value.size
})
// Problems:
// - Creates new instances on every computation
// - Loses previous state
// - Memory leaks
// - Poor performance// GOOD - Create once, use many times
const selection = createSelection()
const selectedCount = computed(() => selection.selected.value.size)❌ Not Using Provided Utilities
// BAD - Manual deep merge
const mergeObjects = (target: any, source: any) => {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {}
mergeObjects(target[key], source[key])
} else {
target[key] = source[key]
}
}
return target
}
// Problems:
// - Not prototype-pollution safe
// - Doesn't handle edge cases
// - Poor performance
// - Reinventing the wheel// GOOD - Use provided utility
import { mergeDeep } from '@vuetify/v0/utilities'
const result = mergeDeep(target, source) // Safe & optimizedComponent Anti-Patterns
❌ Overriding Component Structure
// BAD - Breaking compound component patterns
<Tabs.Root>
<div class="my-custom-wrapper">
<Tabs.List>
<!-- This breaks the component relationship -->
</Tabs.List>
</div>
</Tabs.Root>
// Problems:
// - Breaks ARIA relationships
// - Loses keyboard navigation
// - Breaks focus management
// - Accessibility issues// GOOD - Follow component structure
<Tabs.Root>
<Tabs.List class="my-custom-styles">
<!-- Keep proper structure, add styling -->
</Tabs.List>
</Tabs.Root>❌ Not Using Provided Slots
// BAD - Recreating component functionality
<div class="my-dialog" v-if="open">
<div class="backdrop" @click="close"></div>
<div class="content">
<!-- Manual dialog implementation -->
</div>
</div>
// Problems:
// - No focus trap
// - No ARIA attributes
// - No keyboard handling
// - No portal rendering
// - Accessibility issues// GOOD - Use provided component
<Dialog.Root v-model="open">
<Dialog.Content>
<!-- Proper accessibility & focus management -->
</Dialog.Content>
</Dialog.Root>Migration Strategy
When refactoring existing code:
1. Identify patterns - Look for manual state management 2. Find v0 equivalent - Check decision tree in main skill 3. Replace incrementally - Don't change everything at once 4. Test thoroughly - Ensure behavior is preserved 5. Remove old code - Clean up manual implementations
Quick Fixes
Find and Replace Patterns
# Find manual selection arrays
grep -r "ref<.*\[\]>" --include="*.vue" --include="*.ts"
# Find manual provide/inject
grep -r "inject(" --include="*.vue" --include="*.ts"
# Find window checks
grep -r "typeof window" --include="*.vue" --include="*.ts"---
Lifecycle Gotchas
Register in setup, not onMounted
// Wrong — sibling index is wrong by the time the registry updates
onMounted(() => {
const ticket = single.register({ id, value })
})
// Right — register synchronously so index order matches DOM order
const ticket = single.register({ id, value })Items registered in onMounted fire after all siblings have already registered and the parent has potentially already selected a default. This causes off-by-one indices, incorrect initial selection, and useProxyRegistry rendering in the wrong order.
---
Unregister in onBeforeUnmount, not onUnmounted
// Wrong — context may already be torn down by the time onUnmounted fires
onUnmounted(() => single.unregister(ticket.id))
// Right — context is still reachable in onBeforeUnmount
onBeforeUnmount(() => single.unregister(ticket.id))---
createContext key must contain a colon
// Wrong — ambiguous, may collide with other libraries
export const [useRoot, provideRoot] = createContext('accordion')
// Right — namespaced with colon separator
export const [useRoot, provideRoot] = createContext('v0:accordion')Keys without : violate v0's §9.3 contract and will throw in strict mode.
---
Never create composables inside computed or watch
// Wrong — creates a new registry on every reactive update
const group = computed(() => createGroup())
// Right — call composables once at setup time
const group = createGroup()Composables set up their own internal reactive state on creation. Calling them inside computed or watch creates orphaned instances that accumulate on every re-run and are never cleaned up.
---
Pass props as getters into composables
const { gap } = defineProps<{ gap?: number }>()
// Wrong — captures gap at setup time; prop changes are ignored
createOverflow({ gap })
// Right — getter re-reads the current prop value on every recompute
createOverflow({ gap: () => gap })Any composable option typed as MaybeRefOrGetter<T> must receive a getter, not a destructured value.
Authoring a Compound Component
How to build a new headless compound component on top of v0 primitives.
Pattern overview
Every v0 compound component follows the same four-part structure:
1. `createContext` — establishes the type-safe DI contract between Root and sub-components 2. Root component — holds composable state, provides context 3. Sub-components — consume context, register themselves on mount 4. Lifecycle contract — register() in setup, unregister() in onBeforeUnmount
---
Complete example — Accordion
An accordion with single-open behavior built on createSingle.
1. Define the context
// AccordionRoot.vue — script lang="ts" (top of dual-script SFC)
import { createContext } from '@vuetify/v0'
import type { SingleContext } from '@vuetify/v0'
export interface AccordionContext {
single: SingleContext
}
// [useAccordionRoot, provideAccordionRoot] — the DI pair
export const [useAccordionRoot, provideAccordionRoot] =
createContext<AccordionContext>('v0:accordion')Key rules:
- Key must contain
:—v0:accordion, notaccordion(§9.3) - Use
createContext, never rawprovide/inject(§6.5) - Export the
use*function so sub-components can import it
---
2. Root component
<!-- AccordionRoot.vue -->
<script lang="ts">
import { createContext } from '@vuetify/v0'
import type { SingleContext } from '@vuetify/v0'
export interface AccordionContext {
single: SingleContext
}
export const [useAccordionRoot, provideAccordionRoot] =
createContext<AccordionContext>('v0:accordion')
</script>
<script setup lang="ts">
import { createSingle } from '@vuetify/v0'
const { mandatory = 'keep' } = defineProps<{
mandatory?: 'force' | 'keep'
}>()
const single = createSingle({ mandatory })
// Provide state to all descendants
provideAccordionRoot({ single })
</script>
<template>
<div role="region">
<slot />
</div>
</template>---
3. Item sub-component (trigger + registration)
<!-- AccordionItem.vue -->
<script setup lang="ts">
import { onBeforeUnmount } from 'vue'
import { useAccordionRoot } from './AccordionRoot.vue'
const { id, value } = defineProps<{
id: string
value: string
}>()
// Consume parent context — throws a descriptive error if used outside Root
const { single } = useAccordionRoot()
// Register in setup, not onMounted — so sibling index is correct before DOM paint (§6.8)
const ticket = single.register({ id, value })
// onBeforeUnmount, not onUnmounted — context is still reachable here (§7.3)
onBeforeUnmount(() => single.unregister(ticket.id))
</script>
<template>
<div>
<button
:id="`accordion-trigger-${id}`"
:aria-expanded="single.isSelected(id)"
:aria-controls="`accordion-panel-${id}`"
:data-state="single.isSelected(id) ? 'open' : 'closed'"
@click="single.toggle(id)"
>
<slot name="trigger" />
</button>
<div
v-if="single.isSelected(id)"
:id="`accordion-panel-${id}`"
role="region"
:aria-labelledby="`accordion-trigger-${id}`"
>
<slot />
</div>
</div>
</template>---
4. Consumer usage
<template>
<AccordionRoot mandatory="keep">
<AccordionItem id="section-1" value="Section 1">
<template #trigger>Section 1</template>
Content for section 1
</AccordionItem>
<AccordionItem id="section-2" value="Section 2">
<template #trigger>Section 2</template>
Content for section 2
</AccordionItem>
</AccordionRoot>
</template>---
Using useProxyRegistry for reactive child lists
When the Root needs to react to which children are registered (e.g. to render a list of tabs from registered items), use useProxyRegistry:
<!-- TabsRoot.vue (setup script) -->
<script setup lang="ts">
import { createSingle, useProxyRegistry } from '@vuetify/v0'
const single = createSingle({ mandatory: 'force' })
// Reactive view of the registry — updates when children mount/unmount
const proxy = useProxyRegistry(single.registry, { events: true })
</script>
<template>
<!-- Renders a tab strip from registered items -->
<div role="tablist">
<button
v-for="item in proxy.values"
:key="item.id"
:data-selected="single.isSelected(item.id) || undefined"
@click="single.select(item.id)"
>
{{ item.value }}
</button>
</div>
<slot />
</template>---
v-show vs v-if for registered children
Sub-components that register tickets must stay mounted while logically hidden. Use v-show, not v-if, when a registered child needs to be visually hidden but remain in the registry. v-if triggers onBeforeUnmount, deregisters the ticket, and causes state thrash when the child reappears.
<!-- Wrong — unmounting loses the ticket -->
<AccordionPanel v-if="isOpen" />
<!-- Right — ticket survives the visibility flip -->
<AccordionPanel v-show="isOpen" />Exception: use v-if when the child is structurally absent (never mounted yet, destroyed permanently). Use v-show when it toggles between visible and hidden. See §10.11.
---
Forwarding reactive props into composables
When passing a component prop into a composable option, always wrap it as a getter — never pass the destructured value directly:
const { gap } = defineProps<{ gap?: number }>()
// Wrong — captures gap at setup time, prop changes never propagate
createOverflow({ gap })
// Right — getter re-reads the prop on every recompute
createOverflow({ gap: () => gap })This applies to any composable option typed as MaybeRefOrGetter<T>. See §4.3.
Component Examples
Real-world examples of v0 headless components. All styling uses UnoCSS utility classes.
Styling guide: v0 components are headless — they emit data-* attributes as styling hooks.See the v0 styling docs for UnoCSS setup and data attribute patterns.
Contents
- Tabs Component — compound root/list/item/panel pattern
- Dialog Component — modal with scrim, focus trap, and portal
- Popover Component — floating UI with anchor positioning
- Checkbox Component — controlled + uncontrolled variants
- Radio Component — grouped single-selection
---
Tabs Component
Basic Tabs
<script setup lang="ts">
import { Tabs } from '@vuetify/v0/components'
import { shallowRef } from 'vue'
const activeTab = shallowRef('profile')
const tabs = [
{ value: 'profile', label: 'Profile', component: 'ProfilePanel' },
{ value: 'settings', label: 'Settings', component: 'SettingsPanel' },
{ value: 'billing', label: 'Billing', component: 'BillingPanel' },
]
</script>
<template>
<Tabs.Root v-model="activeTab" class="w-full">
<Tabs.List class="flex border-b-2 border-gray-200">
<Tabs.Item
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
class="px-6 py-3 bg-transparent border-none cursor-pointer border-b-2 border-transparent -mb-0.5 transition-colors data-[selected]:border-blue-600 data-[selected]:text-blue-600 hover:bg-gray-50"
>
{{ tab.label }}
</Tabs.Item>
</Tabs.List>
<Tabs.Panel
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
class="p-6"
>
<component :is="tab.component" />
</Tabs.Panel>
</Tabs.Root>
</template>Vertical Tabs
<script setup lang="ts">
import { Tabs } from '@vuetify/v0/components'
import { shallowRef } from 'vue'
const activeTab = shallowRef('profile')
const tabs = [
{ value: 'profile', label: 'Profile', icon: 'i-mdi-account' },
{ value: 'settings', label: 'Settings', icon: 'i-mdi-cog' },
{ value: 'billing', label: 'Billing', icon: 'i-mdi-credit-card' },
]
</script>
<template>
<Tabs.Root v-model="activeTab" class="h-full">
<div class="flex h-full">
<Tabs.List class="flex flex-col w-48 bg-gray-50 border-r border-gray-200">
<Tabs.Item
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
class="flex items-center gap-2 px-4 py-4 border-none bg-transparent cursor-pointer text-left border-r-3 border-transparent data-[selected]:bg-blue-50 data-[selected]:border-blue-600 hover:bg-gray-100"
>
<span :class="tab.icon" class="text-base" />
{{ tab.label }}
</Tabs.Item>
</Tabs.List>
<div class="flex-1 overflow-y-auto">
<Tabs.Panel
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
class="p-6"
>
<slot :name="tab.value" />
</Tabs.Panel>
</div>
</div>
</Tabs.Root>
</template>---
Dialog Component
Confirmation Dialog
<script setup lang="ts">
import { Dialog } from '@vuetify/v0/components'
import { shallowRef } from 'vue'
const isOpen = shallowRef(false)
function onDelete() {
console.log('Item deleted')
isOpen.value = false
}
</script>
<template>
<Dialog.Root v-model="isOpen">
<Dialog.Activator>
<button class="px-4 py-2 bg-red-600 text-white rounded cursor-pointer hover:bg-red-700">
Delete Item
</button>
</Dialog.Activator>
<Dialog.Content class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-[90%] max-w-md shadow-xl">
<Dialog.Title class="text-xl font-semibold mb-4">
Confirm Deletion
</Dialog.Title>
<Dialog.Description class="text-gray-500 mb-6 leading-relaxed">
This action cannot be undone. Are you sure you want to delete this item?
</Dialog.Description>
<div class="flex gap-3 justify-end">
<Dialog.Close class="px-4 py-2 bg-gray-100 text-gray-800 rounded cursor-pointer hover:bg-gray-200">
Cancel
</Dialog.Close>
<button
class="px-4 py-2 bg-red-600 text-white rounded cursor-pointer hover:bg-red-700"
@click="onDelete"
>
Delete
</button>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
</template>---
Popover Component
Dropdown Menu
<script setup lang="ts">
import { Popover } from '@vuetify/v0/components'
const menuOptions = [
{ value: 'edit', label: 'Edit', icon: 'i-mdi-pencil' },
{ value: 'copy', label: 'Copy', icon: 'i-mdi-content-copy' },
{ value: 'delete', label: 'Delete', icon: 'i-mdi-trash-can' },
]
function onOption(value: string) {
console.log('Selected:', value)
}
</script>
<template>
<Popover.Root>
<Popover.Activator>
<button class="flex items-center gap-2 px-3 py-2 bg-white border border-gray-300 rounded cursor-pointer hover:bg-gray-50">
Options
<span class="i-mdi-chevron-down text-base" />
</button>
</Popover.Activator>
<Popover.Content class="z-10 mt-1">
<div class="bg-white border border-gray-200 rounded shadow-lg min-w-40">
<button
v-for="option in menuOptions"
:key="option.value"
class="flex items-center gap-2 w-full px-4 py-3 border-none bg-transparent text-left text-sm cursor-pointer hover:bg-gray-50 first:rounded-t last:rounded-b"
@click="onOption(option.value)"
>
<span :class="option.icon" class="text-base" />
{{ option.label }}
</button>
</div>
</Popover.Content>
</Popover.Root>
</template>---
Checkbox Component
Custom Styled Checkboxes
<script setup lang="ts">
import { Checkbox } from '@vuetify/v0/components'
import { reactive } from 'vue'
const options = reactive([
{ id: '1', label: 'Email notifications', checked: true },
{ id: '2', label: 'SMS notifications', checked: false },
{ id: '3', label: 'Push notifications', checked: true },
])
</script>
<template>
<div class="flex flex-col gap-3">
<label
v-for="option in options"
:key="option.id"
class="flex items-center gap-3 cursor-pointer"
>
<Checkbox.Root v-model="option.checked">
<Checkbox.Indicator
class="size-4.5 border-2 border-gray-300 rounded flex items-center justify-center transition-colors data-[checked]:bg-blue-600 data-[checked]:border-blue-600"
>
<span class="i-mdi-check text-white text-xs opacity-0 data-[checked]:opacity-100 transition-opacity" />
</Checkbox.Indicator>
</Checkbox.Root>
<span class="text-sm text-gray-700 select-none">{{ option.label }}</span>
</label>
</div>
</template>---
Radio Component
Radio Button Group
<script setup lang="ts">
import { Radio } from '@vuetify/v0/components'
import { shallowRef } from 'vue'
const selected = shallowRef('light')
const themes = [
{ value: 'light', label: 'Light Theme', description: 'Clean and bright interface' },
{ value: 'dark', label: 'Dark Theme', description: 'Easy on the eyes' },
{ value: 'auto', label: 'Auto', description: 'Matches system preference' },
]
</script>
<template>
<Radio.Group v-model="selected" class="flex flex-col gap-4">
<Radio.Root
v-for="theme in themes"
:key="theme.value"
:value="theme.value"
class="flex items-start gap-3 cursor-pointer p-3 rounded-lg border border-gray-200 transition-colors hover:bg-gray-50 data-[checked]:border-blue-600 data-[checked]:bg-blue-50"
>
<Radio.Indicator
class="mt-0.5 size-5 border-2 border-gray-300 rounded-full flex items-center justify-center transition-colors data-[checked]:border-blue-600"
>
<div class="size-2 bg-blue-600 rounded-full opacity-0 data-[checked]:opacity-100 transition-opacity" />
</Radio.Indicator>
<span class="flex-1">
<strong class="block text-base text-gray-900">{{ theme.label }}</strong>
<span class="block text-sm text-gray-500 mt-1">{{ theme.description }}</span>
</span>
</Radio.Root>
</Radio.Group>
</template>Layer Decisions
When to use composables, components, or both.
The stack
Vuetify 4 (design system consumer)
↑
Design Systems (Emerald, Helix, …)
↑
Paper (styling primitives)
↑
v0 — components (WAI-ARIA wrappers)
v0 — composables (state, logic, registries)v0 ships two layers: composables (headless logic) and components (headless UI). Knowing which to reach for is the primary architectural decision.
---
Decision table
| Goal | Use |
|---|---|
| Drop in a standard UI pattern with full accessibility | Component — <Dialog.Root>, <Tabs.Root>, etc. |
| Add selection / step / form logic to a custom component | Composable — createSingle, createSelection, etc. |
| Build a new reusable compound component on top of v0 | Both — composable for state inside a Root component |
| Build a design system (Emerald, Helix, …) | Both — components as reference, composables as foundation |
---
Components — the "just works" path
Reach for components when the built-in compound structure fits your UI.
Every v0 component ships:
- Correct
role,aria-*, andaria-disabledsemantics - Keyboard navigation and roving tabindex where appropriate
- Focus management and
useRovingFocuswiring data-state,data-disabled,data-orientationfor consumer styling<Atom :as :renderless>so consumers can swap the tag or go renderless
<!-- Consumer owns all styling; v0 owns all behavior -->
<Tabs.Root v-model="active">
<Tabs.List class="flex border-b border-gray-200">
<Tabs.Item value="a" class="px-4 py-2 data-[selected]:border-b-2 data-[selected]:border-blue-600">
Tab A
</Tabs.Item>
</Tabs.List>
<Tabs.Panel value="a" class="p-4">Panel A</Tabs.Panel>
</Tabs.Root>---
Composables — the "build it yourself" path
Reach for composables when:
- You're wiring state into a custom component that doesn't match any compound pattern
- You need the logic to live in a parent that isn't a v0 compound root
- You're authoring a new v0-style component (see authoring-guide.md)
// Custom tab bar that uses your own markup
const tabs = createSingle({ mandatory: 'force' })
tabs.register({ id: 'overview', value: 'Overview' })
tabs.register({ id: 'details', value: 'Details' })
tabs.select('overview')
tabs.isSelected('overview') // trueComposables carry zero DOM opinion. They expose state and methods — your component owns the markup and events.
---
Using both — compound component authoring
When building a new reusable compound component, you combine both layers:
- Root uses a composable to hold state and provides context via
createContext - Sub-components consume that context and register themselves
This is exactly how every v0 component is built internally. See authoring-guide.md for a complete walkthrough.
---
Common mistakes
Reaching for a composable when a component is enough
If <Dialog.Root> fits, use it. Rolling your own focus trap and ARIA contract from createContext alone saves nothing and loses accessibility correctness.
Reaching for a component when you need raw logic
If you need selection state shared across siblings that don't form a compound hierarchy, createSingle is the right tool — not wrapping everything in a synthetic <Tabs.Root>.
Skipping v0 entirely
Before writing a ref<string | null>(null) + toggle pattern, check the decision table in the main skill. v0 almost certainly has the composable you need.
Vuetify0 API Reference
Detailed API documentation for @vuetify/v0 composables, components, utilities, and plugins.
Contents
- Composables (selection, forms, foundation, registration, data)
- Browser & Environment (breakpoints, media queries, storage)
- DOM Observation (resize, intersection, mutation observers)
- Event Handling (click outside, hotkeys, event listeners)
- Headless Components (Tabs, Dialog, Popover, Form, Checkbox, Radio, Pagination)
- Utility Functions (mergeDeep, clamp, range, useId)
- Type Guards (isString, isObject, isNullOrUndefined, etc.)
- Plugins (theme, locale, features, permissions, logger, stack, notifications)
---
Composables
Selection Patterns
createSingle — Single Selection
import { createSingle } from '@vuetify/v0'
const selector = createSingle({ mandatory: 'force' })
selector.register({ id: 'light', value: 'light' })
selector.register({ id: 'dark', value: 'dark' })
selector.selectedId // Ref<string | undefined>
selector.selectedValue // Ref<T | undefined>
selector.select('dark')createSelection — Multi Selection
import { createSelection } from '@vuetify/v0'
const selection = createSelection({ multiple: true })
selection.register({ id: 'a', value: 'A' })
selection.register({ id: 'b', value: 'B' })
selection.toggle('a')
selection.selectedIds // Set<string>
selection.isSelected('a') // booleancreateGroup — Multi Selection with Tri-State
import { createGroup } from '@vuetify/v0'
const group = createGroup()
group.selectAll()
group.toggleAll()
group.isMixed // true when partially selected
group.isAllSelectedcreateStep — Sequential Navigation
import { createStep } from '@vuetify/v0'
const stepper = createStep({ circular: true })
stepper.register({ id: 'step1' })
stepper.register({ id: 'step2' })
stepper.register({ id: 'step3' })
stepper.next()
stepper.prev()
stepper.first()
stepper.last()createNested — Hierarchical Selection
import { createNested } from '@vuetify/v0'
const tree = createNested()
tree.register({ id: 'parent', value: 'Parent' })
tree.register({ id: 'child', value: 'Child', parent: 'parent' })
tree.getPath('child') // ['parent', 'child']
tree.getDescendants('parent') // ['child']Form Handling
createForm — Validation
import { createForm } from '@vuetify/v0'
const form = createForm()
const email = form.register({
id: 'email',
value: '',
rules: [
v => !!v || 'Required',
v => /.+@.+/.test(v) || 'Invalid email',
async v => await checkAvailable(v) || 'Email taken'
]
})
email.isValid // true | false | null (pending)
email.errors // string[]
email.validate()
form.submit() // Validates all fieldscreateOtp — OTP / verification code
import { createOtp } from '@vuetify/v0'
const otp = createOtp({
length: 6,
pattern: 'numeric',
onComplete: async value => await verify(value),
})
otp.write(0, '4')
otp.distribute('123456') // returns count consumed
otp.value.value // joined string
otp.isComplete.valueContext & State Sharing
createContext — Type-Safe Provide/Inject
import { createContext } from '@vuetify/v0'
interface ThemeContext {
mode: Ref<string>
toggle: () => void
}
const [useTheme, provideTheme] = createContext<ThemeContext>('Theme')
// Provider
provideTheme({ mode, toggle })
// Consumer (throws helpful error if not provided)
const { mode, toggle } = useTheme()createTrinity — Context with Defaults
import { createTrinity } from '@vuetify/v0'
const [useConfig, provideConfig, defaultConfig] = createTrinity<Config>('Config', {
theme: 'light',
locale: 'en'
})Collection Management
createRegistry — Item Lifecycle
import { createRegistry } from '@vuetify/v0'
const registry = createRegistry<{ id: string; label: string }>()
registry.register({ id: 'item1', label: 'Item 1' })
registry.unregister('item1')
registry.items // Map<string, Item>
registry.ids // string[]Data Utilities
createFilter — Array Filtering
import { createFilter } from '@vuetify/v0'
const { apply } = createFilter({ keys: ['name', 'email'] })
const query = shallowRef('')
const users = shallowRef([...])
const filtered = apply(query, users)createPagination — Page Navigation
import { createPagination } from '@vuetify/v0'
const pagination = createPagination({
page: 1,
itemsPerPage: 10,
length: 100
})
pagination.next()
pagination.prev()
pagination.first()
pagination.last()createVirtual — Virtual Scrolling
import { createVirtual } from '@vuetify/v0'
const { virtualItems, totalHeight, scrollTo } = createVirtual({
items: largeList,
itemHeight: 48
})createTimeline — Undo/Redo
import { createTimeline } from '@vuetify/v0'
const timeline = createTimeline({ maxSize: 50 })
timeline.push(state)
timeline.undo()
timeline.redo()
timeline.canUndo
timeline.canRedocreateQueue — FIFO with Timeouts
import { createQueue } from '@vuetify/v0'
const notifications = createQueue({ timeout: 5000 })
notifications.push({ message: 'Saved!' })
// Auto-removes after 5 seconds---
Browser & Environment
Constants
import {
IN_BROWSER,
SUPPORTS_TOUCH,
SUPPORTS_OBSERVER,
SUPPORTS_INTERSECTION_OBSERVER
} from '@vuetify/v0/constants'
if (IN_BROWSER && SUPPORTS_OBSERVER) {
// Safe to use ResizeObserver
}useBreakpoints
import { useBreakpoints } from '@vuetify/v0'
const { xs, sm, md, lg, xl, xxl, smAndUp, mdAndDown } = useBreakpoints()useMediaQuery
import { useMediaQuery } from '@vuetify/v0'
const { matches } = useMediaQuery('(prefers-color-scheme: dark)')---
DOM Observation
useResizeObserver
import { useResizeObserver } from '@vuetify/v0'
const el = shallowRef<HTMLElement>()
const { width, height, pause, resume } = useResizeObserver(el)useIntersectionObserver
import { useIntersectionObserver } from '@vuetify/v0'
const el = shallowRef<HTMLElement>()
const { isIntersecting } = useIntersectionObserver(el, { threshold: 0.1 })useMutationObserver
import { useMutationObserver } from '@vuetify/v0'
useMutationObserver(el, callback, { childList: true, subtree: true })---
Event Handling
useEventListener
import { useEventListener } from '@vuetify/v0'
useEventListener(window, 'resize', onResize)
// Auto-cleanup on unmountuseHotkey
import { useHotkey } from '@vuetify/v0'
useHotkey('ctrl+k', openCommandPalette)
useHotkey('g-h', navigateHome) // Sequence: g then huseClickOutside
import { useClickOutside } from '@vuetify/v0'
useClickOutside(menuRef, close, { touchThreshold: 500 })---
Headless Components
All components are unstyled, accessible, and follow WAI-ARIA patterns.
Tabs
<script setup>
import { Tabs } from '@vuetify/v0'
</script>
<template>
<Tabs.Root v-model="active">
<Tabs.List>
<Tabs.Item value="overview">Overview</Tabs.Item>
<Tabs.Item value="features">Features</Tabs.Item>
</Tabs.List>
<Tabs.Panel value="overview">...</Tabs.Panel>
<Tabs.Panel value="features">...</Tabs.Panel>
</Tabs.Root>
</template>Dialog
<script setup>
import { Dialog } from '@vuetify/v0'
</script>
<template>
<Dialog.Root v-model="open">
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Confirm</Dialog.Title>
<Dialog.Description>Are you sure?</Dialog.Description>
<Dialog.Close>Cancel</Dialog.Close>
</Dialog.Content>
</Dialog.Root>
</template>Checkbox
<script setup>
import { Checkbox } from '@vuetify/v0'
</script>
<template>
<Checkbox.Root v-model="checked">
<Checkbox.Indicator />
<Checkbox.Label>Accept terms</Checkbox.Label>
</Checkbox.Root>
</template>Radio
<script setup>
import { Radio } from '@vuetify/v0'
</script>
<template>
<Radio.Group v-model="selected">
<Radio.Item value="a">
<Radio.Indicator />
<Radio.Label>Option A</Radio.Label>
</Radio.Item>
<Radio.Item value="b">
<Radio.Indicator />
<Radio.Label>Option B</Radio.Label>
</Radio.Item>
</Radio.Group>
</template>Available Components
| Component | Purpose |
|---|---|
Atom | Polymorphic element (render as any tag) |
Avatar | Image with fallback |
Checkbox | Checkbox control (standalone or group) |
Dialog | Modal overlay with focus trap |
ExpansionPanel | Accordion (single or multi-expand) |
Group | Multi-selection container |
Pagination | Page navigation |
Popover | Toggle overlay (CSS anchor positioning) |
Radio | Radio buttons with roving tabindex |
Scrim | Overlay backdrop |
Selection | Generic selection container |
Single | Single selection container |
Step | Stepper/wizard |
Tabs | Tab navigation |
---
Utility Functions
import {
mergeDeep,
clamp,
range,
useId
} from '@vuetify/v0/utilities'
// Deep merge (prototype pollution safe)
const config = mergeDeep({}, defaults, overrides)
// Clamp number to range
const clamped = clamp(value, 0, 100)
// Create number array
const nums = range(5) // [0, 1, 2, 3, 4]
const nums2 = range(5, 1) // [1, 2, 3, 4, 5]
// SSR-safe unique ID
const id = useId()Type Guards
import {
isString,
isNumber,
isBoolean,
isObject,
isArray,
isFunction,
isNull,
isUndefined,
isNullOrUndefined,
isPrimitive,
isSymbol,
isNaN
} from '@vuetify/v0/utilities'
if (isObject(value)) {
// value is Record<string, unknown>
}---
Plugins
For app-wide features:
import {
createThemePlugin,
createFeaturesPlugin,
createLoggerPlugin,
createLocalePlugin,
createBreakpointsPlugin,
createDatePlugin,
createStoragePlugin
} from '@vuetify/v0'
// In main.ts
app.use(createThemePlugin({
themes: {
light: { colors: { primary: '#3b82f6' } },
dark: { colors: { primary: '#60a5fa' } }
}
}))
// In components
const { current, toggle } = useTheme()Selection Patterns
Complete guide to v0 selection composables with real-world examples.
Contents
- Single Selection — exclusive pick (tabs, theme picker, accordion)
- Multi Selection — non-exclusive pick (filters, tag pickers)
- Group Selection — multi + tri-state select-all (data tables)
- Step Navigation — ordered progression (wizards, carousels)
- Advanced Patterns — mandatory modes, disabled items, value mapping
Single Selection {#single}
For exclusive selection - tabs, theme pickers, accordion panels.
Basic Usage
import { createSingle } from '@vuetify/v0'
const single = createSingle()
// Register items
single.register({ id: 'light', value: 'Light Theme' })
single.register({ id: 'dark', value: 'Dark Theme' })
// Select programmatically
single.select('dark')
// Access selected
single.selectedValue.value // 'Dark Theme'
single.selected.value // Set(['dark'])Mandatory Selection
// Force selection - user can't deselect all
const themePicker = createSingle({ mandatory: 'force' })
// Keep last selection - clicking selected item does nothing
const tabs = createSingle({ mandatory: 'keep' })Theme Switcher Example
<template>
<div class="theme-switcher">
<button
v-for="theme in themes"
:key="theme.id"
:class="{ active: single.isSelected(theme.id) }"
@click="single.select(theme.id)"
>
{{ theme.value }}
</button>
</div>
</template>
<script setup lang="ts">
import { createSingle } from '@vuetify/v0'
const themes = [
{ id: 'light', value: 'Light' },
{ id: 'dark', value: 'Dark' },
{ id: 'auto', value: 'Auto' }
]
const single = createSingle({ mandatory: 'force' })
// Register all themes
themes.forEach(theme => single.register(theme))
// Set initial selection
single.select('light')
</script>Multi Selection {#multi}
For multiple independent selections - tags, filters, permissions.
Basic Usage
import { createSelection } from '@vuetify/v0'
const selection = createSelection({ multiple: true })
// Toggle items
selection.toggle('tag1')
selection.toggle('tag2')
// Check selection
selection.isSelected('tag1') // true
selection.selected.value // Set(['tag1', 'tag2'])Tag Selector Example
<template>
<div class="tag-selector">
<button
v-for="tag in availableTags"
:key="tag"
:class="{ selected: selection.isSelected(tag) }"
@click="selection.toggle(tag)"
>
{{ tag }}
<span v-if="selection.isSelected(tag)">✓</span>
</button>
</div>
</template>
<script setup lang="ts">
import { createSelection } from '@vuetify/v0'
const availableTags = ['vue', 'typescript', 'css', 'javascript']
const selection = createSelection({ multiple: true })
// Register all available tags
availableTags.forEach(tag => {
selection.register({ id: tag, value: tag })
})
</script>Group Selection {#group}
For grouped selection with "select all" functionality - data tables, file managers.
Basic Usage
import { createGroup } from '@vuetify/v0'
const group = createGroup()
// Register items
users.forEach(user => {
group.register({ id: user.id, value: user })
})
// Group operations
group.selectAll() // Select everything
group.unselectAll() // Clear selection
group.toggleAll() // Smart toggle based on current state
// Tri-state logic
group.isAllSelected // true if everything selected
group.isMixed // true if partially selectedData Table Example
<template>
<table>
<thead>
<tr>
<th>
<input
type="checkbox"
:checked="group.isAllSelected"
:indeterminate="group.isMixed"
@click="group.toggleAll()"
/>
</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>
<input
type="checkbox"
:checked="group.isSelected(user.id)"
@click="group.toggle(user.id)"
/>
</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { createGroup } from '@vuetify/v0'
const users = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
]
const group = createGroup()
// Register all users
users.forEach(user => {
group.register({ id: user.id, value: user })
})
</script>Step Navigation {#step}
For sequential navigation - wizards, carousels, onboarding flows.
Basic Usage
import { createStep } from '@vuetify/v0'
const stepper = createStep({
circular: true, // Allow wrap-around
})
// Navigation
stepper.next() // Go forward
stepper.prev() // Go backward
stepper.first() // Jump to start
stepper.last() // Jump to end
// State
stepper.selectedIndex.value // Current step index (-1 if none selected)
stepper.selectedId.value // Id of the current stepWizard Example
<template>
<div class="wizard">
<!-- Progress indicator -->
<div class="steps">
<div
v-for="(step, index) in steps"
:key="index"
:class="{
active: stepper.selectedIndex.value === index,
completed: stepper.selectedIndex.value > index
}"
>
{{ step.title }}
</div>
</div>
<!-- Step content -->
<div class="step-content">
<component :is="currentStep.component" />
</div>
<!-- Navigation -->
<div class="step-nav">
<button
:disabled="stepper.selectedIndex.value <= 0"
@click="stepper.prev()"
>
Previous
</button>
<button
:disabled="stepper.selectedIndex.value >= steps.length - 1"
@click="stepper.next()"
>
Next
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { createStep } from '@vuetify/v0'
import { computed } from 'vue'
const steps = [
{ title: 'Profile', component: 'ProfileStep' },
{ title: 'Preferences', component: 'PreferencesStep' },
{ title: 'Review', component: 'ReviewStep' }
]
const stepper = createStep()
steps.forEach((step, index) => stepper.register({ id: String(index), value: step }))
stepper.first()
const currentStep = computed(() => steps[stepper.selectedIndex.value] ?? steps[0])
</script>Advanced Patterns
Nested Selection
For hierarchical data like file trees:
import { createNested } from '@vuetify/v0'
const tree = createNested()
// Register parent-child relationships
tree.register({ id: 'folder1', value: 'Folder' })
tree.register({ id: 'file1', value: 'File 1', parentId: 'folder1' })
tree.register({ id: 'file2', value: 'File 2', parentId: 'folder1' })
// Selecting parent auto-selects children
tree.select('folder1') // Also selects file1, file2Selection with Dependencies
const selection = createSelection({ multiple: true })
// Custom logic for dependent selections
watch(() => selection.selected.value, (selected) => {
if (selected.has('premium') && !selected.has('basic')) {
selection.select('basic') // Premium requires basic
}
})Persistent Selection
import { createSelection, useStorage } from '@vuetify/v0'
const selection = createSelection({ multiple: true })
const storage = useStorage()
const stored = storage.get<string[]>('selected-items', [])
// Restore saved selection on mount
onMounted(() => {
stored.value.forEach(id => selection.select(id))
})
// Persist selection changes
watch(() => selection.selected.value, selected => {
stored.value = Array.from(selected)
})Related skills
How it compares
Choose vuetify0 over hand-rolled composables when Vuetify-aligned accessibility and observer logic must match the Vuetify 4 roadmap.
FAQ
Does vuetify0 include CSS styling?
vuetify0 provides headless Vue 3 logic only with zero styling; applications supply their own CSS while composables handle selection, validation, observers, and WAI-ARIA behavior.
When should agents load vuetify0?
Load vuetify0 when code references @vuetify/v0, #v0 imports, createX composables, or Vue 3 UI needing headless selection, virtual scrolling, data tables, or keyboard-accessible primitives.