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.
Stack Overview
| Layer | Technology | Purpose |
|---|---|---|
| SSG | vite-ssg↗ | Pre-renders all routes to static HTML |
| Routing | unplugin-vue-router↗ | File-based routing from src/pages/ |
| Markdown | unplugin-vue-markdown↗ + Shiki↗ | Vue components in markdown, syntax highlighting |
| Styling | UnoCSS↗ + presetWind4↗ | Tailwind v4 utilities mapped to v0 tokens |
| State | Pinia↗ | App-level state (drawer, navigation) |
| Logic | @vuetify/v0 | Headless 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 for keyboard navigation.
<script setup lang="ts">
import { createSingle, useProxyRegistry } from '@vuetify/v0'
// Events are required for useProxyRegistry
const single = createSingle({ mandatory: 'force', events: true })
const proxy = useProxyRegistry(single)
function onKeydown (event: KeyboardEvent) {
const tabs = Array.from(proxy.values)
const currentIndex = tabs.findIndex(t => t.isSelected.value)
switch (event.key) {
case 'ArrowLeft':
// Move to previous tab
break
case 'ArrowRight':
// Move to next tab
break
}
}
</script>
<template>
<div role="tablist" @keydown="onKeydown">
<button
v-for="tab in proxy.values"
:key="tab.id"
:aria-selected="tab.isSelected.value"
role="tab"
@click="tab.toggle"
>
{{ tab.value }}
</button>
</div>
</template>Why this works: createSingle handles the selection logic. useProxyRegistry exposes registered items for iteration. The component owns all styling and accessibility attributes.
Mobile Navigation
The AppNav component uses v0 primitives for polymorphism and interaction:
<script setup lang="ts">
import { shallowRef, useTemplateRef } from 'vue'
import { Atom, useClickOutside, useBreakpoints } from '@vuetify/v0'
const navRef = useTemplateRef<AtomExpose>('nav')
const breakpoints = useBreakpoints()
// Close drawer when clicking outside
useClickOutside(
() => navRef.value?.element,
() => {
if (drawer.value && breakpoints.isMobile.value) {
drawer.value = false
}
},
{ ignore: ['[data-app-bar]'] }
)
</script>
<template>
<Atom
ref="nav"
as="nav"
aria-label="Main navigation"
:inert="!drawer && breakpoints.isMobile.value ? true : undefined"
>
<slot />
</Atom>
</template>| Primitive | Role |
|---|---|
| Atom | Polymorphic element—renders as <nav> with ref access |
| useClickOutside | Closes mobile drawer on outside click |
| useBreakpoints | Tracks viewport for responsive behavior |
Interactive Demos
The homepage demo uses Selection to show v0’s component pattern:
<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="{ isSelected, toggle }"
:value="item.id"
>
<button
:class="isSelected ? 'bg-primary' : 'bg-surface'"
@click="toggle"
>
{{ 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:
<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:
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:color-mix(in_srgb,var(--v0-surface)_70%,transparent)] backdrop-blur-12',
},
})This enables:
text-primary→ uses--v0-primarybg-surface→ uses--v0-surface- Theme switching updates all utilities automatically
Accessibility Preflights
Global focus styles and reduced motion support:
*: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↗:
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 tablesDocsApiHover— inline type hints in code blocksvirtual:api— importable API data
Patterns Worth Stealing
1. Composable-First Components
Don’t embed logic in components. Extract to composables, expose via slot props:
<!-- 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:
// 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:
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
apps/docs/
├── build/ # Build-time plugins
│ ├── generate-api.ts # API extraction
│ ├── generate-nav.ts # Navigation tree
│ └── markdown.ts # Shiki + callouts
├── src/
│ ├── components/
│ │ ├── app/ # Shell (AppNav, AppBar)
│ │ ├── docs/ # Doc UI (DocsExample, 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 pipelineSummary
This documentation site demonstrates that v0’s patterns scale from simple toggles to complex applications:
| Pattern | Where Used |
|---|---|
| createSingle + Registry | Tabbed code groups |
| Atom polymorphism | Navigation, buttons, links |
| useClickOutside | Mobile drawer dismissal |
| useStorage | User preferences |
| Selection compound | Interactive demos |
| CSS variable theming | Entire design system |
The same primitives you use for a checkbox work for an entire documentation platform.
