NotForm provides the form state, validation, field state, submission lifecycle, and dynamic array-field primitives for Vue 3 while you decide how the form looks and behaves. There are no opinionated inputs, styles, or UI components built into the core.
Validation is schema-driven supporting any Standard Schema compatible validator, state is reactive, and the API is designed for typed field paths and Vue's Composition API.
| Package | Description |
|---|---|
notform |
Core Vue 3 form management and headless components |
notform-nuxt |
Official Nuxt integration with auto-imports |
pnpm add notformnpx nuxi module add notformThen install a Standard Schema-compatible validator, for example:
pnpm add zod<script setup lang="ts">
import { NotField, NotForm, NotMessage, useNotForm } from 'notform'
import { z } from 'zod'
const schema = z.object({
email: z.email('Enter a valid email address'),
name: z.string('Enter a valid name'),
})
const form = useNotForm({
initialValues: {
email: '',
name: '',
},
onSubmit(values) {
console.log('Submitted:', values)
},
schema,
})
</script>
<template>
<NotForm
:form="form"
@submit="form.submit"
@reset="form.reset()"
>
<NotField
v-slot="{ events, path }"
path="name"
>
<div>
<label :for="path">Name</label>
<input
:id="path"
v-model="form.values.name"
v-bind="events"
name="name"
type="text"
autocomplete="name"
>
<NotMessage :path="path" />
</div>
</NotField>
<NotField
v-slot="{ events, path }"
path="email"
>
<div>
<label :for="path">Email address</label>
<input
:id="path"
v-model="form.values.email"
v-bind="events"
name="email"
type="email"
autocomplete="email"
>
<NotMessage :path="path" />
</div>
</NotField>
<button type="submit">
Submit
</button>
<button type="reset">
Reset
</button>
</NotForm>
</template>NotArrayField provides renderless operations for dynamic arrays while preserving stable item keys during reordering.
<script setup lang="ts">
import { NotArrayField, NotField, NotForm, NotMessage, useNotForm } from 'notform'
import { z } from 'zod'
const tagSchema = z.string()
const schema = z.object({
tags: z.array(tagSchema).optional(),
})
const form = useNotForm({
initialValues: {
tags: [''],
},
onSubmit(values) {
console.log('Submitted:', values)
},
schema,
})
</script>
<template>
<NotForm
:form="form"
@submit="form.submit"
@reset="form.reset()"
>
<NotArrayField
v-slot="{ items, append, remove }"
path="tags"
:item-schema="tagSchema"
>
<div
v-for="(item, index) in items"
:key="item.key"
>
<NotField
v-slot="{ events, path }"
:path="item.path"
>
<label :for="path">Tag {{ index + 1 }}</label>
<input
:id="path"
v-model="form.values.tags[index]"
v-bind="events"
:name="path"
type="text"
>
<NotMessage :path="path" />
</NotField>
<button
type="button"
@click="remove(index)"
>
Remove
</button>
</div>
<button
type="button"
@click="append('')"
>
Add tag
</button>
</NotArrayField>
</NotForm>
</template>This repository is a pnpm workspace managed with Turborepo.
pnpm install
pnpm devCommon checks:
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm ready