21

Quickstart

Build a complete sign-in form with schema validation, field errors, a submit handler, and reset.

This page builds a sign-in form from start to finish. You will define a schema, create a form, bind two inputs, and handle submission.

It assumes you have completed Installation.

1. Define a schema

The schema describes the values the user edits and the rules they must satisfy.

<script setup lang="ts">
import { z } from 'zod'

const signInSchema = z.object({
  email: z.email('Enter a valid email address.'),
  password: z.string().min(8, 'Use at least 8 characters.'),
})
</script>

2. Create the form

useForm takes the schema, the starting values, and a submit handler.

<script setup lang="ts">
import { useForm } from 'notform'

const form = useForm({
  initialValues: { email: '', password: '' },
  async onSubmit(data) {
    await signIn(data)
  },
  schema: signInSchema,
})
</script>
  • initialValues are what the inputs show first. See Schema input and output.
  • onSubmit runs only after the whole schema validates. data is the validated output.

3. Bind the fields

form.field(path) creates a field. Field hands it to your template through a slot.

<template>
  <Field
    v-slot="{ field }"
    :field="form.field('email')"
  >
    <label :for="field.path">Email</label>

    <input
      :id="field.path"
      v-model="field.model"
      v-bind="field.events"
      autocomplete="email"
      type="email"
    >

    <FieldError :errors="field" />
  </Field>
</template>
  • v-model="field.model" reads and writes the value in form.values.
  • v-bind="field.events" spreads onBlur, onChange, and onInput, which tell NotForm when to validate.
  • FieldError shows the first error for this field and renders nothing when there is none.

4. Render and submit

Form renders a native <form>. Native submit calls form.submit, and native reset calls form.reset.

<script setup lang="ts">
import { Field, FieldError, Form, useForm } from 'notform'
import { z } from 'zod'

const signInSchema = z.object({
  email: z.email('Enter a valid email address.'),
  password: z.string().min(8, 'Use at least 8 characters.'),
})

const form = useForm({
  initialValues: { email: '', password: '' },
  async onSubmit(data) {
    await signIn(data)
  },
  schema: signInSchema,
})

async function signIn(data: { email: string, password: string }) {
  await fetch('/api/sign-in', {
    body: JSON.stringify(data),
    headers: { 'content-type': 'application/json' },
    method: 'POST',
  })
}
</script>

<template>
  <Form
    :form="form"
    novalidate
  >
    <Field
      v-slot="{ field }"
      :field="form.field('email')"
    >
      <label :for="field.path">Email</label>

      <input
        :id="field.path"
        v-model="field.model"
        v-bind="field.events"
        autocomplete="email"
        type="email"
      >

      <FieldError :errors="field" />
    </Field>

    <Field
      v-slot="{ field }"
      :field="form.field('password')"
    >
      <label :for="field.path">Password</label>

      <input
        :id="field.path"
        v-model="field.model"
        v-bind="field.events"
        autocomplete="current-password"
        type="password"
      >

      <FieldError :errors="field" />
    </Field>

    <button
      type="submit"
      :disabled="form.isSubmitting"
    >
      {{ form.isSubmitting ? 'Signing in...' : 'Sign in' }}
    </button>

    <button
      type="reset"
      :disabled="form.isSubmitting"
    >
      Reset
    </button>
  </Form>
</template>

novalidate turns off the browser's built-in constraint validation so your schema controls every message.

What you just built

BehaviorWhy it happens
Leaving an empty email field shows an error.Fields validate on blur and change by default.
Typing does not show errors on a pristine field.Input events only validate after the field already has an error.
After an error appears, fixing the value clears it as you type.Default eager mode re-validates on input once a field has an error.
Clicking Sign in with invalid values shows every error and does not call onSubmit.Submit validates the whole form first.
The button is disabled while the request runs.form.isSubmitting stays true until onSubmit settles.
Reset restores the starting values and clears errors.Form calls form.reset() on native reset.

Next steps

Copyright © 2026 NotForm
Published under MIT License