Quickstart

Build a fully typed login form with validation in minutes.

This guide walks you through building a simple login form using the useNotForm composable and NotForm's headless components.

Prefer experimenting in your browser with zero setup? You can test ideas directly in the interactive Playground.

Here is the login form we will be building. Try submitting the form empty, or typing an invalid email, to see the validation in action.

Step-by-step

Define the Schema

First, define your validation schema using a Standard Schema compliant library (this guide uses zod). The schema describes both fields the login form needs — email and password — along with the rules each must satisfy.

import { z } from 'zod'

const schema = z.object({
  email: z.email('Enter a valid email'),
  password: z.string('Invalid input').min(8, 'At least 8 characters'),
})

This schema is the single source of truth for the form: it decides the shape of form.values, drives which field paths are valid to use with <NotField>, and produces the validation messages you'll render later with <NotMessage>.

Initialize the Form

Use the useNotForm composable to create your form instance. Pass in the schema you just defined and an onSubmit handler that runs once validation succeeds.

const form = useNotForm({
  async onSubmit(data) {
    // `data` is fully typed as { email: string, password: string }
    console.log('Form submitted:', data)
  },
  schema,
})

form is the reactive object every other piece of this guide plugs into — <NotForm> reads it for context, <NotField> reads and writes to form.values, and <NotMessage> reads validation errors from it.

Render the Form

Use the <NotForm> component to wrap your form and pass it the form instance via the form prop. <NotForm> renders a native <form> element and automatically prevents the browser's default submit and reset behavior, so binding @submit="form.submit" and @reset="form.reset()" is all you need — no event.preventDefault() required.

<template>
  <NotForm
    :form="form"
    @submit="form.submit"
    @reset="form.reset()"
  >
    <!-- Fields will go here -->

    <button
      type="submit"
      :disabled="form.isSubmitting"
    >
      Submit
    </button>
  </NotForm>
</template>

Connect the Fields

Wrap each input in a <NotField> component. Its default slot exposes two things you'll use on every field: path, a stable string ID for that field (handy for <label for>, the input's id, and <NotMessage>), and events, a set of handlers that track interaction and trigger validation. Spread events onto the input with v-bind — by default this wires up validation on blur and on change, so errors appear once the user leaves the field or edits it, not on every keystroke.

Add both fields from the schema — email and password — the same way:

<template>
  <NotField
    v-slot="{ events, path }"
    path="email"
  >
    <div>
      <label :for="path">Email</label>

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

      <NotMessage :path="path" />
    </div>
  </NotField>

  <NotField
    v-slot="{ events, path }"
    path="password"
  >
    <div>
      <label :for="path">Password</label>

      <input
        :id="path"
        v-model="form.values.password"
        v-bind="events"
        type="password"
      >

      <NotMessage :path="path" />
    </div>
  </NotField>
</template>
<NotMessage> automatically displays the first active validation error for the given path, and renders nothing when that field has no errors — there's no need to write your own v-if around it.

Putting it together

With both fields wired in place of the comment from the "Render the Form" step, you have a complete, fully typed, validating login form: submitting with empty or invalid values shows inline errors, and submitting with valid values calls onSubmit with typed, validated data.

Next Steps

To learn more about how NotForm integrates into your Vue applications, check out the Components guide for the full API of <NotForm>, <NotField>, <NotArrayField>, and <NotMessage>, or deep dive into Composables to see everything form exposes beyond submit and reset.