Getting started
Verific validates an application-owned model with any Standard Schema schema. It returns structured issues for logic and error strings for display; it does not own input state, touched state or submission.
Choose a task
- Validate one form: continue with the complete example on this page.
- Compose descendant registrations: learn how scopes collect nested forms.
- Render or localise errors: configure localised messages while keeping accessible markup under your control.
- Use Verific with Nuxt: configure the Nuxt module once for an application.
Install
pnpm add @verific/core zodnpm install @verific/core zodyarn add @verific/core zodNo plugin is required. The following form uses Zod, but any Standard Schema-compatible library can supply the schema.
Try validation in the browser
This example is the complete useValidation(schema, model) workflow. Try it in three states:
- Leave the empty Email address field.
validateFor('email')runs the complete schema but publishes only the email issue; the untouched password remains quiet. - Select Validate account without entering anything.
validate()publishes the complete form result and moves focus to the first invalid input. - Enter a valid email address and a password of at least eight characters, then validate again. The errors clear and the form reports a valid outcome.
View the source used by this example
<script setup lang="ts">
import { useValidation } from '@verific/core'
import { computed, nextTick, ref } from 'vue'
import { z } from 'zod'
const schema = z.object({
email: z.string()
.min(1, 'Enter your email address')
.refine(value => value === '' || z.email().safeParse(value).success, 'Enter a valid email address'),
password: z.string().min(8, 'Use at least 8 characters'),
})
const email = ref('')
const password = ref('')
const { errorsFor, hasError, isValidating, issues, result, validate, validateFor } = useValidation(schema, { email, password })
const outcome = computed(() => {
const issueCount = issues.value.length
if (result.value.status === 'idle' && issueCount === 0)
return 'Submit the form to validate it.'
if (result.value.status === 'valid' && issueCount === 0)
return 'The account details are valid.'
if (issueCount === 0)
return 'No field errors are shown. Submit the form to confirm.'
return `Please resolve ${issueCount} validation ${issueCount === 1 ? 'error' : 'errors'}.`
})
async function focusFirstInvalid(path: readonly PropertyKey[] | undefined) {
await nextTick()
const field = path?.[0]
if (field === 'email' || field === 'password') {
document.getElementById(`basic-${field}`)?.focus()
}
}
async function onSubmit() {
const result = await validate()
if (!result.success) {
await focusFirstInvalid(result.issues[0]?.path)
}
}
</script>
<template>
<div class="verific-example">
<form novalidate @submit.prevent="onSubmit">
<div class="verific-example__grid">
<div class="verific-example__field">
<label for="basic-email">Email address</label>
<input
id="basic-email"
v-model="email"
type="email"
autocomplete="email"
:aria-invalid="hasError('email')"
aria-describedby="basic-email-errors"
@blur="validateFor('email')"
>
<ul id="basic-email-errors" class="verific-example__errors" aria-live="polite" aria-atomic="true">
<li v-for="(error, index) in errorsFor('email')" :key="`${index}:${error}`">
{{ error }}
</li>
</ul>
</div>
<div class="verific-example__field">
<label for="basic-password">Password</label>
<input
id="basic-password"
v-model="password"
type="password"
autocomplete="new-password"
:aria-invalid="hasError('password')"
aria-describedby="basic-password-errors"
@blur="validateFor('password')"
>
<ul id="basic-password-errors" class="verific-example__errors" aria-live="polite" aria-atomic="true">
<li v-for="(error, index) in errorsFor('password')" :key="`${index}:${error}`">
{{ error }}
</li>
</ul>
</div>
</div>
<div class="verific-example__actions">
<button type="submit" :disabled="isValidating">
{{ isValidating ? 'Validating…' : 'Validate account' }}
</button>
</div>
<p class="verific-example__outcome" role="status" aria-live="polite">
{{ outcome }}
</p>
</form>
</div>
</template>The component owns its refs and submission state. validateFor(path) is useful for blur or change events: it captures the complete model and runs the complete Standard Schema, then publishes issues only at that exact path. validate() publishes the full result and remains the method to await before submission. Both methods may be asynchronous.
Before localisation is configured, errorsFor() returns the prose supplied by the schema. See localising errors when the form is working.
The model in five terms
- A scope is everything validated by one
validate()call. - A registration is one schema and model pair in that scope. The example above creates both the scope and its first registration.
- A schema failure becomes a structured issue.
- A message resolver turns an issue into a ready-to-render error string. Schema prose is the fallback.
- A successful registration may expose transformed output from the schema; Verific does not write it back to the model.
For a form split across components, continue with scopes and registrations. The useValidation page in Reference documents the complete controller interface.
