← Blog
31 Jul 2026Vueform validationVue 3DOM Studiofrontend development

Vue Form Validation: Build Accessible, Testable Forms with DOM Studio

Learn Vue form validation with DOM Studio: set rules, show field errors, validate on submit, handle server errors, and scale with schemas.

Vue Form Validation: Build Accessible, Testable Forms with DOM Studio

Vue form validation works best when we treat it as a shared contract, not a collection of one-off checks. In this guide, we will build a Vue 3 account form that tracks values, validates fields at the right time, prevents invalid submission, and maps backend failures back to the field that needs attention.

We will use DOM Studio because its form components keep field presentation, form state, validation, and nested paths connected. Vue’s v-model remains the value binding, while DomForm aggregates the state around it.

Before we start

Prerequisites

  • A Vue 3 project using Single File Components.
  • DOM Studio available in the project.
  • A server endpoint that validates submitted data independently of the browser.

What we will validate

  • Name: required, at least two characters.
  • Email: required and correctly formatted for the UI.
  • Password: required, at least 12 characters.
  • Email uniqueness: checked by the server.

Client-side validation makes the interaction clearer and faster. It does not make the input trustworthy. We should enforce the same critical constraints on the server before any data is persisted.

1. Define rules, ownership, and validation timing

Start by separating rules into two groups:

  1. Field rules answer whether one value is present and shaped correctly, such as required, email format, minimum length, or a fixed pattern.
  2. Form and server rules answer whether the values work together or are permitted by the product, such as matching passwords, unique email addresses, plan limits, and permissions.

We recommend validating a field after blur for most forms, then validating every required field again on submit. This avoids showing errors before a person has had a chance to finish typing, while still giving immediate feedback once they leave the control.

Verification check: write each rule beside the layer that owns it. If a rule affects security, authorization, uniqueness, or persisted data, it belongs on the server even if we also run a client-side version.

Troubleshooting: do not use a client-only check for “email is available” or “user may select this role.” Browser code can be changed or bypassed.

Screenshot of getdom.studio

2. Build a form with named DOM Studio fields

A name is the connection point between a DOM Studio field and its nearest DomForm. The form collects named values, errors, and validity, so we can submit a single predictable object instead of manually coordinating each input.

<script setup>
import { ref } from 'vue'
import {
  DomButton,
  DomEmailInput,
  DomForm,
  DomPasswordInput,
  DomTextInput,
} from '@getdom/studio/vue'

const account = ref({
  name: '',
  email: '',
  password: '',
})

const feedback = ref('')

function onSubmit({ values }) {
  feedback.value = `Ready to create an account for ${values.email}.`
}

function onInvalid({ errors }) {
  const count = Object.keys(errors).length
  feedback.value = `Fix ${count} field${count === 1 ? '' : 's'} before continuing.`
}
</script>

<template>
  <DomForm
    v-model="account"
    class="space-y-4"
    @submit="onSubmit"
    @invalid="onInvalid"
  >
    <DomTextInput
      name="name"
      label="Name"
      autocomplete="name"
      required
      :validators="[
        {
          name: 'minLength',
          props: { min: 2 },
          message: 'Enter at least 2 characters.',
        },
      ]"
    />

    <DomEmailInput
      name="email"
      label="Email"
      autocomplete="email"
      required
    />

    <DomPasswordInput
      name="password"
      label="Password"
      autocomplete="new-password"
      required
      :validators="[
        {
          name: 'minLength',
          props: { min: 12 },
          message: 'Use at least 12 characters.',
        },
      ]"
    />

    <DomButton type="submit">Create account</DomButton>

    <p v-if="feedback" role="status" class="text-sm text-muted-fg">
      {{ feedback }}
    </p>
  </DomForm>
</template>

Expected result: entering values updates account; an invalid submit fires @invalid; a valid submit fires @submit with the aggregated values object.

Verification check: submit an empty form, then correct the fields one at a time. Confirm that the visible error state follows the field that needs correction and that no success handler runs until the form is valid.

Troubleshooting: if a value is missing from the submitted object, check that the field sits inside the intended DomForm and has a unique name.

The DOM Studio Forms overview explains the shared field contract, and the DomForm reference documents its state, events, nested paths, and programmatic API.

Watercolour illustration of a reusable form field wrapper connected to input and validation state components.

3. Trigger validation deliberately

Validation timing affects the quality of the interaction. A field that validates on every keystroke can be useful for a compact, unambiguous rule. It can also be distracting for longer values, passwords, or server-backed checks.

For most account forms, we use this sequence:

  1. Let the person type without interruption.
  2. Validate when the field blurs.
  3. Revalidate the whole form on submit.
  4. Use a debounce before any network-based validation.

DOM Studio fields can participate in form-level validation and can also be validated from code. Give the form a name when another action, such as a multi-step “Continue” button, must test the current step.

<script setup>
import { ref } from 'vue'
import { DomButton, DomForm, DomTextInput, forms } from '@getdom/studio/vue'

const profile = ref({ displayName: '' })
const stepMessage = ref('')

async function continueToNextStep() {
  const valid = await forms.profileStep?.validate()
  stepMessage.value = valid
    ? 'This step is valid.'
    : 'Review the highlighted field before continuing.'
}
</script>

<template>
  <DomForm name="profileStep" v-model="profile">
    <DomTextInput
      name="displayName"
      label="Display name"
      required
      :validators="[
        { name: 'minLength', props: { min: 2 } },
      ]"
    />

    <DomButton type="button" @click="continueToNextStep">
      Continue
    </DomButton>

    <p v-if="stepMessage" role="status">{{ stepMessage }}</p>
  </DomForm>
</template>

Expected result: clicking Continue validates the registered fields and leaves the user on the current step if any rule fails.

Troubleshooting: do not make a server request for every input event. Validate local format first, then debounce remote checks and ignore stale responses that return after the value has changed.

4. Return server errors to the correct field

A successful client-side validation means only that the current browser data satisfies the client rules. The server still needs to validate the request, apply business rules, and return structured errors.

For example, an account endpoint may respond with a duplicate-email error. We can place that message in DOM Studio’s runtime field state without rewriting the authored field props.

<script setup>
import { forms } from '@getdom/studio/vue'

async function createAccount(values) {
  const response = await fetch('/api/accounts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(values),
  })

  if (response.ok) return true

  const payload = await response.json()

  if (payload.errors?.email) {
    forms.accountForm?.setFieldState('email', {
      invalid: true,
      errors: { unique: payload.errors.email },
    })
  }

  return false
}
</script>

Wire createAccount into the valid submit handler from Step 2, and set name="accountForm" on that DomForm.

Expected result: an unavailable email is announced and displayed beside the email field, while the password and name remain untouched.

Verification check: have the API return { "errors": { "email": "That email is already registered." } }. Confirm that the email field becomes invalid and that the message disappears after a corrected value and successful retry.

Troubleshooting: normalize API error keys to the same names or paths used by the form. For nested data, a field path such as invitees.0.email needs a matching server error key.

5. Keep custom controls in the same validation contract

A date picker, slug editor, file widget, or button group should not force us to create a separate error and accessibility system. In DOM Studio, custom controls can use useField() for state and attributes, then let DomField present the label, description, required state, and errors.

<script setup>
import { DomField, fieldProps, useField } from '@getdom/studio/vue'

const props = defineProps({
  ...fieldProps,
  modelValue: { type: String, default: '' },
})

const emit = defineEmits(['update:modelValue', 'blur', 'focus'])
const field = useField(props, emit, { idPrefix: 'workspace-slug' })
</script>

<template>
  <DomField v-bind="field.fieldAttrs.value">
    <input
      v-bind="field.inputAttrs.value"
      class="skin-input"
      @input="field.onInput($event.target.value)"
      @focus="field.onFocus"
      @blur="field.onBlur"
    />
  </DomField>
</template>

Expected result: the custom input works on its own with v-model, or registers with a parent form when it has a name.

Troubleshooting: avoid duplicating labels and errors in both the wrapper and the custom input. Bind fieldAttrs to DomField and inputAttrs to the actual control, then keep display responsibility in one place.

For deeper component customization, see the DomField reference.

Watercolour workflow diagram showing validation from user input through client rules and server verification.

6. Move repeatable forms to a schema when the shape grows

Hand-authored fields are ideal for a focused form. Once we are rendering a configurable onboarding flow, admin editor, or data-driven setup screen, we should separate the form definition from the values that users submit.

DOM Studio can generate renderable form children from a Zod-like shape or JSON Schema. The model stays as plain Vue data, while the schema describes types, required fields, constraints, and component choices.

<script setup>
import { ref } from 'vue'
import { DomForm, zodSchemaToChildren } from '@getdom/studio/vue'

const account = ref({
  name: '',
  email: '',
  plan: 'starter',
})

const schema = {
  type: 'object',
  shape: {
    name: { type: 'string', minLength: 2, description: 'Name' },
    email: { type: 'string', format: 'email', description: 'Email' },
    plan: {
      type: 'enum',
      values: ['starter', 'team', 'enterprise'],
      description: 'Plan',
    },
  },
}

const children = zodSchemaToChildren(schema)
</script>

<template>
  <DomForm v-model="account" :children="children" />
</template>

Expected result: the field definition can evolve without mixing UI metadata into the submitted account object.

Troubleshooting: keep the schema and model separate. A form definition describes how to render and validate data; the model should contain only the data the user has entered.

Read the DOM Studio schema guide before adopting a schema-driven approach across a product.

When VeeValidate or Vuelidate may be the better fit

DOM Studio is a strong fit when we want editable UI primitives, a unified field contract, programmatic form APIs, and schema-to-component rendering. It is not the only sound approach.

  • Choose VeeValidate when an existing Vue interface needs a validation-focused, UI-agnostic Composition API. Its useForm() and defineField() approach is useful when we want to keep our current component library and add typed schemas.
  • Consider Vuelidate when its model-based validation style matches an existing codebase or team preference.
  • Keep native HTML semantics such as appropriate input types, autocomplete values, labels, and submit behavior regardless of the validation library.

For a short alternate walkthrough, watch the Vuelidate video below. The principles transfer even though the API differs.

Finished: a validation flow we can test and extend

We now have a Vue form validation flow that collects named values, gives people clear field feedback, blocks invalid submits, supports programmatic checks, and accepts authoritative server errors. That is a durable base for sign-up, settings, onboarding, and operational forms.

Next action: take one existing Vue form and apply this sequence: identify its server-owned rules, give every control a stable field name, test invalid submit behavior, then add one integration test for a backend field error. When we standardize that loop, form validation becomes easier to maintain as the product grows.

Ready to turn that pattern into reusable application UI? Start with DOM Studio’s form primitives and apply the same field contract across your product.