Skip to main content

Quickstart

Get started with Vuetify Zero by building a functional checkbox component in just a few minutes.

Prerequisites

Before starting, make sure you have:
1

Installed Vuetify Zero

Follow the installation guide if you haven’t already:
npm install @vuetify/v0@latest
2

Set up a Vue 3 project

You can use Vite, Nuxt, or any Vue 3.5+ setup:
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install

Your First Component

Let’s build a functional checkbox component to understand Vuetify Zero’s core concepts.

Step 1: Import the Checkbox

Vuetify Zero uses compound components with sub-components for maximum flexibility:
App.vue
<script setup lang="ts">
import { Checkbox } from '@vuetify/v0'
import { ref } from 'vue'

const agreed = ref(false)
</script>

<template>
  <div class="container">
    <h1>My First Vuetify Zero Component</h1>
    
    <label class="checkbox-label">
      <Checkbox.Root v-model="agreed" class="checkbox">
        <Checkbox.Indicator class="indicator">

        </Checkbox.Indicator>
      </Checkbox.Root>
      
      <span>I agree to the terms and conditions</span>
    </label>
    
    <p v-if="agreed" class="success">
      Thank you for agreeing!
    </p>
  </div>
</template>

Step 2: Add Styling

Vuetify Zero is headless—you control all styling. Add CSS to style your checkbox:
App.vue
<style scoped>
.container {
  max-width: 600px;
  margin: 2rem auto;
  padding: 2rem;
  font-family: system-ui, sans-serif;
}

h1 {
  font-size: 2rem;
  margin-bottom: 1.5rem;
  color: #333;
}

.checkbox-label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  font-size: 1rem;
}

.checkbox {
  width: 1.25rem;
  height: 1.25rem;
  border: 2px solid #ddd;
  border-radius: 0.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
}

.checkbox[data-state="checked"] {
  background: #3b82f6;
  border-color: #3b82f6;
}

.indicator {
  color: white;
  font-size: 0.875rem;
  opacity: 0;
  transition: opacity 0.2s;
}

.checkbox[data-state="checked"] .indicator {
  opacity: 1;
}

.success {
  margin-top: 1rem;
  padding: 0.75rem;
  background: #dcfce7;
  border: 1px solid #86efac;
  border-radius: 0.375rem;
  color: #166534;
}
</style>

Step 3: Run Your App

Start your development server:
npm run dev
You should see a styled, functional checkbox that:
  • Responds to clicks
  • Updates the agreed ref
  • Shows a success message when checked
  • Has smooth transitions
Notice the data-state attribute? Vuetify Zero components expose state through data attributes for easy styling.

Understanding the Component

Compound Components

Vuetify Zero uses compound components for flexibility:
<Checkbox.Root>       <!-- Container with logic -->
  <Checkbox.Indicator> <!-- Visual indicator -->

  </Checkbox.Indicator>
</Checkbox.Root>
Each sub-component has a specific role:
  • Checkbox.Root - Manages state and accessibility
  • Checkbox.Indicator - Visual indicator (only shown when checked)
  • Checkbox.HiddenInput - Native input for forms (auto-rendered when name prop is set)

Data Attributes

Components expose state through data-* attributes:
/* Unchecked state */
.checkbox[data-state="unchecked"] { }

/* Checked state */
.checkbox[data-state="checked"] { }

/* Indeterminate state */
.checkbox[data-state="indeterminate"] { }

/* Disabled state */
.checkbox[data-disabled] { }

v-model Support

All Vuetify Zero components support v-model for two-way binding:
<script setup>
const checked = ref(false)
</script>

<template>
  <Checkbox.Root v-model="checked">
    <!-- Will automatically sync with checked ref -->
  </Checkbox.Root>
</template>

Try More Components

Dialog Component

Create an accessible modal dialog:
<script setup lang="ts">
import { Dialog } from '@vuetify/v0'
</script>

<template>
  <Dialog.Root>
    <Dialog.Activator class="btn-primary">
      Open Dialog
    </Dialog.Activator>
    
    <Dialog.Content class="dialog">
      <Dialog.Title class="dialog-title">
        Confirm Action
      </Dialog.Title>
      
      <Dialog.Description class="dialog-description">
        Are you sure you want to proceed?
      </Dialog.Description>
      
      <div class="dialog-actions">
        <Dialog.Close class="btn-secondary">
          Cancel
        </Dialog.Close>
        
        <Dialog.Close class="btn-primary">
          Confirm
        </Dialog.Close>
      </div>
    </Dialog.Content>
  </Dialog.Root>
</template>

<style scoped>
.btn-primary {
  padding: 0.5rem 1rem;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
}

.dialog {
  margin: auto;
  padding: 1.5rem;
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 0.5rem;
  max-width: 28rem;
  width: 100%;
}

.dialog-title {
  font-size: 1.25rem;
  font-weight: 600;
  margin-bottom: 0.5rem;
}

.dialog-description {
  color: #6b7280;
  margin-bottom: 1.5rem;
}

.dialog-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-end;
}

.btn-secondary {
  padding: 0.5rem 1rem;
  background: white;
  color: #374151;
  border: 1px solid #e5e7eb;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  cursor: pointer;
}
</style>

Using Composables

Vuetify Zero composables let you build custom logic:
TaskManager.vue
<script setup lang="ts">
import { createSelection } from '@vuetify/v0'
import { ref } from 'vue'

interface Task {
  id: string
  label: string
}

const selection = createSelection<Task>({
  multiple: true,
  mandatory: false
})

const tasks = [
  { id: '1', label: 'Write documentation' },
  { id: '2', label: 'Review pull requests' },
  { id: '3', label: 'Update dependencies' },
]

// Register tasks with selection
tasks.forEach(task => {
  selection.register({ id: task.id, value: task })
})

function deleteSelected() {
  const selected = Array.from(selection.selectedValues.value)
  console.log('Deleting:', selected)
  selection.unselectAll()
}
</script>

<template>
  <div class="task-manager">
    <h2>Task Manager</h2>
    
    <div class="task-list">
      <label
        v-for="task in tasks"
        :key="task.id"
        class="task-item"
      >
        <input
          type="checkbox"
          :checked="selection.selectedIds.has(task.id)"
          @change="selection.toggle(task.id)"
        >
        {{ task.label }}
      </label>
    </div>
    
    <button
      v-if="selection.selectedIds.size > 0"
      class="delete-btn"
      @click="deleteSelected"
    >
      Delete {{ selection.selectedIds.size }} selected
    </button>
  </div>
</template>

<style scoped>
.task-manager {
  max-width: 32rem;
  margin: 2rem auto;
  padding: 1.5rem;
  border: 1px solid #e5e7eb;
  border-radius: 0.5rem;
}

h2 {
  margin-bottom: 1rem;
  font-size: 1.5rem;
}

.task-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.task-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem;
  border: 1px solid #e5e7eb;
  border-radius: 0.375rem;
  cursor: pointer;
}

.delete-btn {
  width: 100%;
  padding: 0.75rem;
  background: #ef4444;
  color: white;
  border: none;
  border-radius: 0.375rem;
  font-weight: 500;
  cursor: pointer;
}
</style>

Key Concepts

Headless Components

No imposed styling—you have complete control over appearance using CSS, Tailwind, UnoCSS, or any styling solution.

Compound Components

Components are split into sub-components (Root, Item, Content, etc.) for maximum flexibility and composition.

Accessibility Built-in

ARIA attributes, keyboard navigation, and focus management are handled automatically following WAI-ARIA best practices.

TypeScript Support

Full type safety with generics, interfaces, and comprehensive type definitions for all components and composables.

Common Patterns

Slot Props

Access component state through slot props:
<Dialog.Root v-slot="{ isOpen, open, close }">
  <button @click="open">Open</button>
  <p v-if="isOpen">Dialog is open</p>
  <button @click="close">Close</button>
</Dialog.Root>

Custom Styling with Data Attributes

Style based on component state:
/* Style based on selection state */
.item[data-selected] {
  background: blue;
  color: white;
}

/* Style based on disabled state */
.item[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Style based on checkbox state */
.checkbox[data-state="indeterminate"] .indicator {
  background: gray;
}

Form Integration

Use the name prop for automatic form integration:
<form @submit.prevent="handleSubmit">
  <Checkbox.Root
    v-model="agreed"
    name="terms"
    value="accepted"
  >
    <Checkbox.Indicator>✓</Checkbox.Indicator>
  </Checkbox.Root>
  
  <!-- Hidden input automatically rendered -->
  <button type="submit">Submit</button>
</form>
When you provide a name prop, Vuetify Zero automatically renders a hidden input for native form submission.

Next Steps

Now that you understand the basics, explore more:

Components

Explore all 18 headless components with examples

Composables

Learn about 40+ composables for building custom logic

Styling Guide

Master styling patterns with CSS, Tailwind, and more

API Reference

Dive deep into component props, slots, and events

Build docs developers (and LLMs) love