Skip to main content

Design Principles

Vuetify Zero is built on five core principles that guide every architectural decision. Understanding these principles helps you make the most of the framework.

1. Headless First

Principle: Separate behavior from presentation. Provide logic and accessibility without imposing styles.

What This Means

Components provide:
  • ✅ State management
  • ✅ Accessibility (ARIA attributes, keyboard navigation)
  • ✅ Focus management
  • ✅ Event handling
Components do NOT provide:
  • ❌ CSS styles
  • ❌ Class names (except data attributes)
  • ❌ Markup structure beyond minimal semantic HTML

Why It Matters

<!-- Traditional styled component -->
<VButton color="primary" size="large">
  Click me
</VButton>
<!-- Output: Pre-styled button with specific classes -->

<!-- Headless component -->
<Selection.Item value="item" v-slot="{ attrs, isSelected, toggle }">
  <button 
    v-bind="attrs"
    :class="isSelected ? 'my-active' : 'my-inactive'"
    @click="toggle"
  >
    Click me
  </button>
</Selection.Item>
<!-- Output: Your markup with behavior -->
Benefits:
  • Complete design freedom
  • Smaller bundle size (no CSS to ship)
  • Works with any styling approach
  • No style override battles

Implementation

All components expose behavior via slot props:
<Tabs.Item value="home" v-slot="{ attrs, isSelected, toggle }">
  <!-- You provide the markup -->
  <button 
    v-bind="attrs"          <!-- Accessibility attributes -->
    @click="toggle"         <!-- Behavior method -->
    :class="/* your styles */"
  >
    Home
  </button>
</Tabs.Item>
Always spread v-bind="attrs" - this is where all accessibility magic happens.

2. Slot-Driven

Principle: Maximum flexibility through comprehensive scoped slot APIs.

What This Means

Every component provides:
  • Scoped slots with state and methods
  • Data attributes for styling hooks
  • Renderless mode for complete control

Why It Matters

Slots give you complete control over rendering:
<script setup lang="ts">
  import { Dialog } from '@vuetify/v0/components'
</script>

<template>
  <Dialog.Root>
    <!-- Control activator rendering -->
    <Dialog.Activator v-slot="{ attrs }">
      <button v-bind="attrs" class="my-button">
        Open
      </button>
    </Dialog.Activator>
    
    <!-- Control content rendering -->
    <Dialog.Content v-slot="{ attrs }">
      <div v-bind="attrs" class="my-modal">
        <Dialog.Title class="my-title">Title</Dialog.Title>
        <Dialog.Description class="my-description">
          Description
        </Dialog.Description>
      </div>
    </Dialog.Content>
  </Dialog.Root>
</template>

Implementation

Common slot props:
Component TypeSlot Props
Selection.Itemattrs, isSelected, select, unselect, toggle
Group.Itemattrs, isSelected, isMixed, toggle
Tabs.Itemattrs, isSelected, isActive
Dialog.Activatorattrs, isOpen
Checkbox.Rootattrs, isSelected, isIndeterminate
Data attributes for styling:
[data-selected="true"] { /* Selected state */ }
[data-disabled="true"] { /* Disabled state */ }
[data-mixed="true"] { /* Tri-state (indeterminate) */ }
[data-orientation="vertical"] { /* Orientation */ }

3. CSS Variables for Theming

Principle: All configurable styling through CSS custom properties with --v0-* prefix.

What This Means

When v0 components need default styles (rare), they use CSS variables:
/* Component uses CSS variables */
.v0-component {
  color: var(--v0-color-primary);
  background: var(--v0-color-surface);
  border-radius: var(--v0-radius-md);
}

Why It Matters

  • Runtime theming without recompilation
  • No build-time configuration needed
  • Works with any CSS approach
  • Easy dark mode support

Implementation

import { createThemePlugin } from '@vuetify/v0/composables'

app.use(
  createThemePlugin({
    default: 'light',
    themes: {
      light: {
        colors: {
          primary: '#1976d2',
          secondary: '#424242',
          surface: '#ffffff',
          background: '#f5f5f5'
        }
      },
      dark: {
        colors: {
          primary: '#2196f3',
          secondary: '#616161',
          surface: '#1e1e1e',
          background: '#121212'
        }
      }
    }
  })
)
Theme plugin injects CSS variables:
:root {
  --v0-color-primary: #1976d2;
  --v0-color-secondary: #424242;
  --v0-color-surface: #ffffff;
  --v0-color-background: #f5f5f5;
}

[data-theme="dark"] {
  --v0-color-primary: #2196f3;
  --v0-color-secondary: #616161;
  --v0-color-surface: #1e1e1e;
  --v0-color-background: #121212;
}
Most v0 components are completely unstyled. CSS variables are only used when components need minimal default styles (like focus rings).

4. TypeScript Native

Principle: Full type safety with generics for extensibility. Zero any types.

What This Means

Every API is fully typed:
  • Props with generics
  • Slot props with inference
  • Return types
  • Event payloads

Why It Matters

import { createSelection } from '@vuetify/v0/composables'
import type { SelectionTicketInput } from '@vuetify/v0/composables'

// Define custom ticket type
interface MyItem extends SelectionTicketInput {
  label: string
  category: 'fruit' | 'vegetable'
  metadata?: Record<string, unknown>
}

// Type-safe selection
const selection = createSelection<MyItem>()

selection.register({
  label: 'Apple',
  category: 'fruit',  // ✅ Type-safe
  // category: 'meat', // ❌ Type error
  metadata: { color: 'red' }
})

// Type-safe access
const ticket = selection.get('...')
if (ticket) {
  ticket.label     // string
  ticket.category  // 'fruit' | 'vegetable'
  ticket.metadata  // Record<string, unknown> | undefined
}

Implementation

Generic constraints throughout:
// Registry with generic ticket type
export function createRegistry<
  Z extends RegistryTicketInput = RegistryTicketInput,
  E extends RegistryTicket & Z = RegistryTicket & Z,
>(options?: RegistryOptions): RegistryContext<Z, E>

// Selection extends registry
export function createSelection<
  Z extends SelectionTicketInput = SelectionTicketInput,
  E extends SelectionTicket<Z> = SelectionTicket<Z>,
>(options?: SelectionOptions): SelectionContext<Z, E>

// Components with generic v-model
<script lang="ts" setup generic="T">
  import type { SelectionProps } from './types'
  
  defineProps<SelectionProps>()
  const model = defineModel<T | T[]>()
</script>
Slot type inference:
<Selection.Item 
  value="apple" 
  v-slot="{ 
    attrs,      // Record<string, unknown>
    isSelected, // Readonly<Ref<boolean>>
    toggle      // () => void
  }"
>
  <!-- Fully typed slot props -->
</Selection.Item>

5. Composable Architecture

Principle: Reusable logic through Vue 3 composables. Components are thin wrappers.

What This Means

Every component is built on a composable:

Why It Matters

You can use the same logic with or without components:
<script setup lang="ts">
  import { Selection } from '@vuetify/v0/components'
  import { ref } from 'vue'
  
  const selected = ref([])
</script>

<template>
  <Selection.Root v-model="selected" multiple>
    <Selection.Item value="apple">Apple</Selection.Item>
    <Selection.Item value="banana">Banana</Selection.Item>
  </Selection.Root>
</template>

Implementation

Components use composables internally:
<!-- Selection.Root component -->
<script lang="ts" setup generic="T">
  import { createSelectionContext } from '@vuetify/v0/composables'
  import { useProxyModel } from '@vuetify/v0/composables'
  
  const props = defineProps<SelectionProps>()
  const model = defineModel<T | T[]>()
  
  // Create context using composable
  const [, provideSelection, context] = createSelectionContext({
    namespace: 'v0:selection',
    multiple: props.multiple,
    mandatory: props.mandatory
  })
  
  provideSelection(context)
  
  // Bridge to v-model
  useProxyModel(context, model, { multiple: props.multiple })
</script>

<template>
  <slot />
</template>
This architecture enables:
  • Using composables directly when components are too opinionated
  • Building custom components on top of composables
  • Testing logic without mounting components
  • Non-Vue usage (Pinia stores, route guards, utilities)

Additional Principles

Minimal Dependencies

Only Vue 3.5+ required. Optional dependencies:
  • Markdown libraries (for markdown parsing features)
  • Date adapters (for date manipulation)
{
  "dependencies": {
    "vue": ">=3.5.0"
  },
  "peerDependencies": {
    "vue": ">=3.5.0"
  }
}

Tree-Shakeable

Subpath exports enable aggressive tree-shaking:
// Import only what you need
import { createSelection } from '@vuetify/v0/composables'
import { Tabs } from '@vuetify/v0/components'
import { isObject } from '@vuetify/v0/utilities'

// Unused exports are eliminated from bundle

SSR-Safe

All code works in SSR environments:
import { IN_BROWSER } from '@vuetify/v0/constants'

// SSR-safe browser detection
if (IN_BROWSER) {
  // Browser-only code
  window.addEventListener('resize', handler)
}

Performance-First

Minimal reactivity by default:
// Registry collections are NOT reactive
const items = registry.values() // Snapshot

// Selection state IS reactive
const selection = createSelection()
selection.selectedIds // Reactive Set

// Opt-in to reactivity when needed
const registry = createRegistry({ reactive: true })
Lazy caching for computed values:
// First call: O(n)
const keys = registry.keys()

// Subsequent calls: O(1) until mutation
const keys2 = registry.keys()
Batch operations for bulk updates:
// Batch: 1 cache invalidation instead of N
registry.batch(() => {
  items.forEach(item => registry.register(item))
})

// Or use onboard (uses batch internally)
registry.onboard(items)

Design Philosophy in Practice

Example: Building a Data Table

These principles combine to enable flexible composition:
import { 
  createSelection,   // Composable architecture
  createFilter,      // Composable architecture
  createPagination   // Composable architecture
} from '@vuetify/v0/composables'
import { ref, computed } from 'vue'

// Composable architecture: Combine primitives
const selection = createSelection({ multiple: true })
const filter = createFilter()
const pagination = createPagination({ itemsPerPage: 10 })

const query = ref('')
const items = ref([...])

// TypeScript native: Fully typed
const filtered = computed(() => 
  filter.apply(items.value, query.value, item => item.name)
)

const paginated = computed(() => {
  const start = pagination.pageStart.value
  const end = pagination.pageStop.value
  return filtered.value.slice(start, end)
})

// Headless first + Slot-driven: Render however you want
<template>
  <!-- Headless first: Your markup -->
  <div class="data-table">
    <!-- Slot-driven: Full control -->
    <input v-model="query" placeholder="Filter..." />
    
    <table>
      <tbody>
        <tr 
          v-for="item in paginated"
          @click="selection.toggle(item.id)"
          :class="{ 
            'selected': selection.selectedIds.has(item.id) 
          }"
        >
          <td>{{ item.name }}</td>
          <td>{{ item.email }}</td>
        </tr>
      </tbody>
    </table>
    
    <div class="pagination">
      <button @click="pagination.prev()">Prev</button>
      <span>Page {{ pagination.page.value }}</span>
      <button @click="pagination.next()">Next</button>
    </div>
  </div>
</template>

<style scoped>
  /* CSS variables: Easy theming */
  .selected {
    background: var(--v0-color-primary);
    color: var(--v0-color-on-primary);
  }
</style>

Summary

Vuetify Zero’s five core principles:
  1. Headless First - Behavior without styling
  2. Slot-Driven - Maximum flexibility through slots
  3. CSS Variables - Runtime theming with --v0-*
  4. TypeScript Native - Full type safety with generics
  5. Composable Architecture - Components are thin wrappers
Additional principles:
  • Minimal dependencies (only Vue 3.5+)
  • Tree-shakeable (subpath exports)
  • SSR-safe (browser detection guards)
  • Performance-first (minimal reactivity, lazy caching)
These principles work together to create a framework that is:
  • Flexible - Use your own styles and markup
  • Type-safe - Catch errors at compile time
  • Performant - Small bundles, minimal reactivity
  • Composable - Mix and match primitives
  • Accessible - ARIA and keyboard navigation built-in

Build docs developers (and LLMs) love