The Input supports text, email, password, and other native input types. Validation rules run on blur by default, with lazy and eager modifiers available.
We'll never share your email.
Theme: Vuetify0
Theme
Mode
Palettes
Accessibility
Protanopia
Deuteranopia
Tritanopia
<script setup lang="ts">
import { Input } from '@vuetify/v0'
import { shallowRef } from 'vue'
const email = shallowRef('')
</script>
<template>
<div class="flex flex-col gap-1 max-w-sm mx-auto">
<Input.Root
id="email"
v-model="email"
label="Email"
:rules="[
v => !!v || 'Email is required',
v => /.+@.+\..+/.test(v as string) || 'Must be a valid email',
]"
type="email"
>
<label class="text-sm font-medium text-on-surface" for="email">
Email
</label>
<Input.Control
class="w-full px-3 py-2 rounded-lg border border-divider bg-surface text-on-surface placeholder:text-on-surface-variant/50 outline-none data-[focused]:border-primary data-[state=invalid]:border-error transition-colors"
placeholder="you@example.com"
/>
<Input.Description class="text-xs text-on-surface-variant">
We'll never share your email.
</Input.Description>
<Input.Error v-slot="{ errors }" class="text-xs text-error">
<span v-for="error in errors" :key="error">{{ error }}</span>
</Input.Error>
</Input.Root>
</div>
</template>
Root creates a validation context, provides it to children, and manages focus/validation lifecycle. Control is the native <input> (or any element via as). Description and Error auto-wire their IDs into Control’s ARIA attributes.
A multi-field form showing createForm integration, lazy validation, and server-side error injection. Each Input.Root creates its own createValidation instance and auto-registers with the parent Form on mount — no manual wiring is required.
The validateOn="blur lazy" modifier defers validation until the first blur, then validates on every subsequent blur. This avoids showing errors while the user is still typing their first attempt. After the initial blur, the field becomes responsive again so corrections are caught immediately.
Server-side error injection is demonstrated on the email field: entering taken@example.com and submitting causes the composable to push an error string through the :error-messages prop. This pattern keeps client-side rules and server responses in the same display channel without any custom error UI.
File
Role
useContact.ts
Composable — form instance, field refs, submit with server-side validation
ContactForm.vue
Reusable component — three Input fields with different rules and types
contact-form.vue
Demo — wires composable to form, shows submitted data
Try submitting with taken@example.com to see server-side error injection.
Debounced search with validateOn="input" for real-time validation as the user types. The composable watches the Input’s value ref directly — no event wiring needed.
Because Input.Root exposes value as a plain writable Ref, any composable can watch it without subscribing to DOM events. useSearch.ts uses useTimer from @vuetify/v0 for debouncing — staying within the Vuetify0 primitive layer rather than reaching for raw setTimeout. Minimum 2 characters are required before the search fires.
Visual states are driven exclusively through data attributes: data-[focused]:border-primary and data-[state=invalid]:border-error on Input.Control with no slot props or conditional classes. This is the recommended styling approach — it keeps the template clean and decouples visual states from layout logic.
File
Role
useSearch.ts
Composable — debounced search with mock results, watches the query ref
SearchInput.vue
Reusable component — Input with search icon, loading spinner, result count
Control when validation runs with the validateOn prop and optional lazy/eager modifiers:
vue
<template> <!-- Validate on blur (default) --> <Input.Root validate-on="blur" /> <!-- Validate on every keystroke --> <Input.Root validate-on="input" /> <!-- Only validate on form submit --> <Input.Root validate-on="submit" /> <!-- Lazy: skip validation until first blur, then validate on blur --> <Input.Root validate-on="blur lazy" /> <!-- Eager: after first error, validate on every keystroke --> <Input.Root validate-on="blur eager" /></template>
validate-on="blur lazy" skips validation until the first blur, then validates on every blur afterward — so errors don’t show while the user is still typing their first attempt. eager flips it: after the first error, the field validates on every keystroke.
Set the error and error-messages props on Input.Root. They push the message through the same display channel as client-side rules, so no custom error UI is needed.
Input.Root exposes value as a plain writable Ref. A composable can watch it directly — there’s no @input to bind, since composables never attach DOM listeners.
Discord
Need help? Join our community for support and discussions ↗