Skip to main content
Vuetify0 v1.0 is here
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 This Documentation

This documentation site is itself a proof of concept for v0. Every pattern documented here is used to build the site you’re reading.

Edit this page
Report a Bug
Open issues
Copy Markdown

IntermediateAug 16, 2026

Stack Overview

LayerTechnologyPurpose
SSGvite-ssg↗︎Pre-renders all routes to static HTML
Routingvue-router↗︎File-based routing from src/pages/ (built-in since v5)
Markdownunplugin-vue-markdown↗︎ + Shiki↗︎Vue components in markdown, syntax highlighting
StylingUnoCSS↗︎ + presetWind4↗︎Tailwind v4 utilities mapped to v0 tokens
StatePinia↗︎App-level state (drawer, navigation)
Logic@vuetify/v0Headless components and composables

v0 in Action

Tabbed Code Groups

The DocsCodeGroup component powers all tabbed code examples. It uses createSingle for exclusive selection and useProxyRegistry so registered tabs are iterable for rendering and keyboard focus.

DocsCodeGroup.vue
<script setup lang="ts">
  import { createSingle, useProxyRegistry } from '@vuetify/v0'
  import { useId } from 'vue'

  // events: true is required for useProxyRegistry snapshots
  const single = createSingle({ mandatory: 'force', events: true })
  const proxy = useProxyRegistry(single)
  const uid = useId()

  // Manual activation: arrows move focus only; Enter/Space selects (native button)
  function onKeydown (event: KeyboardEvent) {
    const tabs = Array.from(proxy.values)
    const currentIndex = tabs.findIndex(t => t.isSelected.value)
    let nextIndex = currentIndex

    switch (event.key) {
      case 'ArrowLeft':
        nextIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1
        event.preventDefault()
        break
      case 'ArrowRight':
        nextIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0
        event.preventDefault()
        break
      case 'Home':
        nextIndex = 0
        event.preventDefault()
        break
      case 'End':
        nextIndex = tabs.length - 1
        event.preventDefault()
        break
      default:
        return
    }

    if (nextIndex !== currentIndex) {
      document.querySelector<HTMLButtonElement>(
        `#${CSS.escape(`${uid}-tab-${tabs[nextIndex].id}`)}`,
      )?.focus()
    }
  }
</script>

<template>
  <div role="tablist" @keydown="onKeydown">
    <button
      v-for="tab in proxy.values"
      :id="`${uid}-tab-${tab.id}`"
      :key="tab.id"
      :aria-selected="tab.isSelected.value"
      role="tab"
      :tabindex="tab.isSelected.value ? 0 : -1"
      @click="tab.toggle"
    >
      {{ tab.value }}
    </button>
  </div>
</template>
Note

Why this works: createSingle owns exclusive selection. useProxyRegistry exposes tickets for iteration — keyboard handling is hand-rolled roving tabindex + manual activation on top of that list. The component owns styling and the remaining ARIA wiring.

Mobile Navigation

The AppNav component composes v0 primitives for interaction, overlay stacking, and SSR-safe responsiveness:

AppNav.vue
<script setup lang="ts">
  import { onMounted, shallowRef, useTemplateRef } from 'vue'
  import { IN_BROWSER, useClickOutside, useStack, useWindowEventListener } from '@vuetify/v0'
  import { useNavigation } from '@/composables/useNavigation'

  const navigation = useNavigation()
  const navRef = useTemplateRef<HTMLElement>('nav')
  const stack = useStack()

  // Match Tailwind's md breakpoint (768px) for mobile detection
  const isMobile = shallowRef(true)

  function updateMobile () {
    if (!IN_BROWSER) return
    isMobile.value = window.innerWidth < 768
  }

  onMounted(updateMobile)
  useWindowEventListener('resize', updateMobile, { passive: true })

  // Coordinate z-index with other overlays; dismiss when popped off the stack (mobile only)
  const ticket = stack.register({ onDismiss: () => navigation.close() })

  // Close the drawer when clicking outside it on mobile
  useClickOutside(
    () => navRef.value,
    () => {
      if (navigation.isOpen.value && isMobile.value) {
        navigation.close()
      }
    },
    { ignore: ['[data-app-bar]'] },
  )
</script>

<template>
  <nav
    ref="nav"
    aria-label="Main navigation"
    :inert="!navigation.isOpen.value && isMobile ? true : undefined"
    :style="{ zIndex: isMobile ? ticket.zIndex.value : undefined }"
  >
    <slot />
  </nav>
</template>
PrimitiveRole
useClickOutsideCloses the mobile drawer when a click lands outside it
useStackCoordinates overlay z-index and dismissal on stack pop
useWindowEventListenerSSR-safe resize listener driving mobile detection
IN_BROWSERGuards window access during server render
Note

The shipped AppNav additionally wraps its root in a Discovery.Activator (docs tour system) and hand-rolls isMobile against Tailwind’s md breakpoint rather than useBreakpoints, since the nav’s visibility already keys off the same 768px threshold in CSS.

Interactive Demos

The homepage demo uses Selection to show v0’s component pattern:

vue
<script setup lang="ts">
  import { ref } from 'vue'
  import { Selection } from '@vuetify/v0'

  const items = [
    { id: 1, label: 'Option A' },
    { id: 2, label: 'Option B' },
    { id: 3, label: 'Option C' },
  ]

  const model = ref<number[]>([])
</script>

<template>
  <Selection.Root v-model="model" multiple>
    <Selection.Item
      v-for="item in items"
      :key="item.id"
      v-slot="{ attrs, isSelected }"
      :value="item.id"
    >
      <button
        v-bind="attrs"
        :class="isSelected ? 'bg-primary' : 'bg-surface'"
      >
        {{ item.label }}
      </button>
    </Selection.Item>
  </Selection.Root>
</template>

The demo renders live on the homepage—same code, same component, real interactivity.

Persistent Preferences

User preferences (like API display mode) persist across sessions using useStorage:

vue
<script setup lang="ts">
  import { useStorage } from '@vuetify/v0'

  const storage = useStorage()
  const apiMode = storage.get<'inline' | 'links'>('api-display', 'inline')

  function toggleApiMode() {
    apiMode.value = apiMode.value === 'inline' ? 'links' : 'inline'
  }
</script>

No localStorage boilerplate. SSR-safe. Reactive.

UnoCSS + v0 Theming

The docs map UnoCSS utilities to v0’s CSS variable system:

uno.config.ts
import { defineConfig, presetWind4 } from 'unocss'

export default defineConfig({
  presets: [presetWind4()],
  theme: {
    colors: {
      'primary': 'var(--v0-primary)',
      'surface': 'var(--v0-surface)',
      'on-primary': 'var(--v0-on-primary)',
      'on-surface': 'var(--v0-on-surface)',
      // ... all v0 tokens
    },
  },
  shortcuts: {
    'bg-glass-surface': '[background:var(--v0-glass-surface)] backdrop-blur-12',
  },
})

This enables:

  • text-primary → uses --v0-primary

  • bg-surface → uses --v0-surface

  • Theme switching updates all utilities automatically

Accessibility Preflights

Global focus styles and reduced motion support:

css
*:focus-visible {
  outline: 2px solid var(--v0-primary);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Build-Time API Extraction

Component and composable APIs are extracted at build time using vue-component-meta↗︎ and ts-morph↗︎:

build/generate-api.ts
import { createChecker } from 'vue-component-meta'
import { Project } from 'ts-morph'

// Extract props, events, slots from components
const checker = createChecker(tsconfigPath)
const meta = checker.getComponentMeta(componentPath)

// Extract function signatures from composables
const project = new Project({ tsConfigFilePath })
const sourceFile = project.getSourceFileOrThrow(composablePath)

This powers:

  • DocsApi — auto-generated API tables

  • DocsApiHover — inline type hints in code blocks

  • virtual:api — importable API data

Patterns Worth Stealing

1. Composable-First Components

Don’t embed logic in components. Extract to composables, expose via slot props:

vue
<!-- Bad: Logic trapped in component -->
<template>
  <TabGroup @change="handleChange">
    <Tab>One</Tab>
  </TabGroup>
</template>

<!-- Good: Logic accessible, component is delivery -->
<script setup lang="ts">
  import { createSingle } from '@vuetify/v0'

  const single = createSingle()
</script>

<template>
  <Single.Root :single>
    <Single.Item v-slot="{ isSelected, toggle }">
      <button @click="toggle">One</button>
    </Single.Item>
  </Single.Root>
</template>

2. Utility-First with Semantic Tokens

Map utilities to semantic tokens, not raw colors:

text
// Bad: Raw colors
'bg-blue-500'

// Good: Semantic tokens
'bg-primary'  // → var(--v0-primary)

3. SSR-Safe Composables

All v0 composables handle SSR. Use the same patterns:

ts
import { useStorage, useWindowEventListener } from '@vuetify/v0'

// useWindowEventListener checks IN_BROWSER internally
useWindowEventListener('resize', handler)

// useStorage returns reactive ref, works on server
const storage = useStorage()
const pref = storage.get('key', 'default')

File Structure

text
apps/docs/
├── build/                 # Build-time plugins (selected)
│   ├── generate-api.ts    # API extraction
│   ├── generate-nav.ts    # Navigation tree
│   ├── generate-search-index.ts  # Search index
│   └── markdown.ts        # Shiki + callouts
├── src/
│   ├── components/
│   │   ├── app/           # Shell (AppNav, AppBar)
│   │   ├── docs/          # Doc UI (DocsGenesisExample, DocsApi)
│   │   └── home/          # Homepage sections
│   ├── composables/       # App-specific composables
│   ├── examples/          # Live code examples
│   ├── layouts/           # Page layouts
│   ├── pages/             # File-based routes
│   └── stores/            # Pinia stores
├── uno.config.ts          # UnoCSS configuration
└── vite.config.ts         # Build pipeline

Summary

This documentation site demonstrates that v0’s patterns scale from simple toggles to complex applications:

PatternWhere Used
createSingle + RegistryTabbed code groups
Atom polymorphismButtons, links, dividers
useClickOutsideMobile drawer dismissal
useStackMobile drawer z-index coordination
useStorageUser preferences
Selection compoundInteractive demos
CSS variable themingEntire design system

The same primitives you use for a checkbox work for an entire documentation platform.

Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/