Form
A controlled, data-driven form with zero dependencies. Declare validation rules per field; errors map into the wrapped Field automatically. Validates on submit and — after the first submit — live on blur and change.
Sign-up form with validation
Submit with empty or invalid fields to see errors appear inline, then
correct them and watch the errors clear live. The confirm field uses a
custom cross-field validate rule.
import { useState } from "react";
import { Form, FormField, Input, Button, Stack } from "@usevyre/react";
const [values, setValues] = useState({ email: "", password: "", confirm: "" });
<Form values={values} onChange={setValues} onSubmit={() => save(values)}>
<FormField name="email" label="Email" rules={{ required: true, email: true }}>
<Input type="email" placeholder="you@example.com" />
</FormField>
<FormField
name="password"
label="Password"
hint="At least 8 characters"
rules={{ required: true, minLength: 8 }}
>
<Input type="password" placeholder="••••••••" />
</FormField>
<FormField
name="confirm"
label="Confirm password"
rules={{
required: true,
validate: (v, all) =>
v === all.password ? null : "Passwords do not match",
}}
>
<Input type="password" placeholder="••••••••" />
</FormField>
<Stack direction="row" gap="sm" justify="end">
<Button type="submit" variant="primary">Create account</Button>
</Stack>
</Form> <script setup>
import { ref } from "vue";
import { Form, FormField, Input, Button, Stack } from "@usevyre/vue";
const values = ref({ email: "", password: "", confirm: "" });
</script>
<template>
<Form v-model="values" @submit="save(values)">
<FormField
name="email"
label="Email"
:rules="{ required: true, email: true }"
v-slot="{ value, onInput, onBlur }"
>
<Input
:model-value="value"
@update:model-value="onInput"
@blur="onBlur"
type="email"
placeholder="you@example.com"
/>
</FormField>
<FormField
name="password"
label="Password"
hint="At least 8 characters"
:rules="{ required: true, minLength: 8 }"
v-slot="{ value, onInput, onBlur }"
>
<Input
:model-value="value"
@update:model-value="onInput"
@blur="onBlur"
type="password"
/>
</FormField>
<Stack direction="row" gap="sm" justify="end">
<Button type="submit" variant="primary">Create account</Button>
</Stack>
</Form>
</template> Form props
Props
| Prop | Type | Default | Description |
|---|---|---|---|
values | Record<string, any> | — | Controlled values map. Omit for uncontrolled. |
defaultValues | Record<string, any> | {} | Initial values when uncontrolled. |
onChange | (values) => void | — | Fires whenever any field value changes. |
onSubmit | (values) => void | Promise | — | Fires on submit ONLY when the form is valid. |
onInvalid | (errors) => void | — | Fires on submit when validation fails. |
class | string | — | Additional CSS class. |
FormField props
Wrap a single control. FormField injects
name/value/onChange/onBlur
and renders it inside a Field with the right label and error
state.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Key into the form's values map (required). |
label | string | — | Label rendered by the wrapping Field. |
hint | string | — | Helper text shown when there is no error. |
rules | object | — | required | minLength | maxLength | min | max | pattern (RegExp) | email | validate(value, allValues) => string | null. |
Zero dependencies. Validation is built in — no zod/yup
needed. For anything custom,
rules.validate(value, allValues)
returns an error string or null.
Common AI mistakes
- Manually tracking each field's error state with useState→ Wrap controls in <FormField name rules> and let Form manage errors
- Adding a validation library (zod/yup) just for basic rules→ Use rules={{ required, minLength, pattern, email, validate }}
- <FormField> with multiple control children→ Use one control per FormField (Input/Textarea/Select/etc.)
- <FormField> outside a <Form>→ Always nest FormField inside <Form>
Quick examples
Controlled sign-in form with built-in rules
const [values, setValues] = useState({ email: "", password: "" });
<Form values={values} onChange={setValues} onSubmit={(v) => signIn(v)}>
<FormField name="email" label="Email" rules={{ required: true, email: true }}>
<Input type="email" />
</FormField>
<FormField name="password" label="Password" rules={{ required: true, minLength: 8 }}>
<Input type="password" />
</FormField>
<Button type="submit" variant="primary">Sign in</Button>
</Form>Custom cross-field validation
<FormField
name="confirm"
label="Confirm password"
rules={{
required: true,
validate: (v, all) => v === all.password ? null : "Passwords do not match",
}}
>
<Input type="password" />
</FormField>