Input
A headless text input component with integrated validation. Creates a createValidation context internally and auto-registers with parent createForm instances.
Usage
The Input supports text, email, password, and other native input types. Validation rules run on blur by default, with lazy and eager modifiers available.
<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: string) => !!v || 'Email is required',
(v: string) => /.+@.+\..+/.test(v) || '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>
Anatomy
<script setup lang="ts">
import { Input } from '@vuetify/v0'
</script>
<template>
<Input.Root>
<Input.Control />
<Input.Description />
<Input.Error />
</Input.Root>
</template>Architecture
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.
Examples
Contact Form
Multi-field form with createForm integration, lazy validation, and server-side error injection. Each Input.Root creates its own createValidation and auto-registers with the parent form.
File breakdown:
| 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 |
Key patterns:
validateOn="blur lazy"defers validation until the user blurs the field for the first time, then validates on every subsequent blur:errorand:error-messagesinject server-side errors after submit — the email field shows “already registered” whentaken@example.comis usedInput.Control as="textarea"renders the message field as a native textarea while keeping all validation and ARIA wiring
Try submitting with taken@example.com to see server-side error injection.
Live Search
Debounced search with validateOn="input" for real-time validation. The composable watches the Input’s value ref directly — no event wiring needed.
File breakdown:
| 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 |
search.vue | Demo — renders SearchInput with a result list |
Key patterns:
validateOn="input"validates on every keystroke (minimum 2 characters)The composable watches the query ref with
debouncefrom@vuetify/v0/utilities, demonstrating thatvalueis a standard writable Refdata-[focused]:border-primaryanddata-[state=invalid]:border-errorstyle the input purely through data attributes — no slot props needed for visual states
Accessibility
Input.Control renders as a native <input> and manages all ARIA attributes automatically.
ARIA Attributes
| Attribute | Value | Notes |
|---|---|---|
aria-invalid | true | When validation fails or error prop is set |
aria-label | Label text | From Root’s label prop |
aria-describedby | Description ID | Only present when Input.Description is mounted |
aria-errormessage | Error ID | Only present when Input.Error is mounted and errors exist |
aria-required | true | From Root’s required prop |
required | true | Native attribute, from Root’s required prop |
disabled | true | Native attribute, from Root’s disabled prop |
readonly | true | Native attribute, from Root’s readonly prop |
Keyboard Navigation
Standard native <input> keyboard behavior. No custom key handlers — the browser handles focus, selection, and editing.
Input.Root
Props
Events
update:model-value
[value: string]update:isFocused
[value: boolean]Slots
default
InputRootSlotPropsInput.Control
Props
Slots
default
InputControlSlotPropsInput.Description
Props
Slots
default
InputDescriptionSlotPropsInput.Error
Props
Slots
default
InputErrorSlotPropsRecipes
validateOn Modes
Control when validation runs with the validateOn prop and optional lazy/eager modifiers:
<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>Manual Error State
Override validation with the error and error-messages props for server-side errors:
<template>
<Input.Root
:error="!!serverError"
:error-messages="serverError"
:rules="[(v) => !!v || 'Required']"
>
<Input.Control />
<Input.Error v-slot="{ errors }">
<span v-for="e in errors" :key="e">{{ e }}</span>
</Input.Error>
</Input.Root>
</template>Data Attributes
Style interactive states without slot props:
<template>
<Input.Control class="data-[focused]:border-primary data-[state=invalid]:border-error" />
</template>| Attribute | Values | Components |
|---|---|---|
data-state | pristine, valid, invalid | Root, Control |
data-dirty | true | Root |
data-focused | true | Root, Control |
data-disabled | true | Root, Control |
data-readonly | true | Root, Control |