Skip to content
Patternsbeta

Forms & validation

How WeDance forms are laid out, when they show errors and how those errors are worded. Built from Field, Input, Select, Textarea, Checkbox and useFormValidation.

Example: book a private lesson

Submit it empty to see validation. Nothing is sent — this demo only validates in the browser.

Book a private lesson

With Alösha · 60 min · Munich

The teacher replies here.

0/500

vue
<script setup lang="ts">
const form = reactive({ name: '', email: '', style: '', date: '', message: '', terms: false })
const { errors, validate, fieldAttrs } = useFormValidation(LessonSchema, form)

function submit() {
  const result = validate()          // shows every error, focuses the first
  if (!result.success) return
  book(result.data)                  // trimmed, typed data
}
</script>

<template>
  <form class="space-y-5" novalidate @submit.prevent="submit">
    <Field id="lesson-name" label="Your name" :error="errors.name">
      <Input v-model="form.name" autocomplete="name" v-bind="fieldAttrs('name', 'lesson-name-error')" />
    </Field>
    <Field id="lesson-email" label="Email" hint="The teacher replies here." :error="errors.email">
      <Input v-model="form.email" type="email" autocomplete="email" v-bind="fieldAttrs('email', 'lesson-email-error')" />
    </Field>
    <div class="grid gap-5 sm:grid-cols-2">
      <Field id="lesson-style" label="Style" :error="errors.style">
        <Select v-model="form.style" :options="styles" placeholder="Pick a style" v-bind="fieldAttrs('style', 'lesson-style-error')" />
      </Field>
      <Field id="lesson-date" label="Preferred date" :error="errors.date">
        <Input v-model="form.date" type="date" v-bind="fieldAttrs('date', 'lesson-date-error')" />
      </Field>
    </div>
    <Field id="lesson-message" label="What would you like to work on?" optional :error="errors.message">
      <Textarea v-model="form.message" :rows="3" v-bind="fieldAttrs('message', 'lesson-message-error')" />
    </Field>
    <Field id="lesson-terms" :error="errors.terms">
      <Checkbox v-model="form.terms" v-bind="fieldAttrs('terms', 'lesson-terms-error')">I accept the booking terms</Checkbox>
    </Field>
    <Button type="submit" size="lg" class="w-full sm:w-auto">Request lesson</Button>
  </form>
</template>

Layout

  • One columnPeople read forms top to bottom. Pair fields side by side only when they belong together and are short (date + time, first + last name) — and stack them below 640px.
  • Label above the fieldNever beside it, never as placeholder. Works at every width and with long translations.
  • 20px between fieldsspace-y-5 on the form. Group long forms into sections with an h3, not with extra boxes.
  • One primary buttonAt the end, left-aligned with the fields, full width on phones. Its text says what happens: "Request lesson", not "Submit".

Required and optional

Most of our fields are required, so we mark the exceptions.

Do — Mark optional fields with “(optional)”. Ask only for what you need — every optional field is a candidate for deletion.

Don't — Scatter red asterisks on every field. They add noise and nobody remembers the legend.

Validation timing

Exactly what useFormValidation does — don't reinvent it per page.

  1. 1
    While typing, before the first submit

    No errors. Don’t shout at people who haven’t finished.

  2. 2
    On submit with problems

    Every invalid field shows its message at once, and focus moves to the first one.

  3. 3
    While fixing

    Each message disappears live as soon as its field is valid — no need to submit again to see progress.

  4. 4
    After a successful submit or reset()

    Errors are hidden again, so clearing the form doesn’t light it up red.

Always put novalidate on the form so browser bubbles don't compete with our messages. Server errors that belong to a field go into that field's error; anything else goes in one message above the button.

Error message copy

Say what to do, in plain words, in one short sentence ending with a full stop. Messages live in shared/validation so the same mistake reads the same everywhere.

Instead ofWrite
Invalid inputEnter a valid email address.
This field is requiredName is required.
Error: datePick a date for the lesson.
URL format incorrect!Enter a full link starting with https://
  • Missing value: “Field is required.” or a verb: “Choose a dance style.”
  • Wrong format: show the right one — “Enter a full link starting with https://”.
  • No blame, no jargon, no “oops”, no exclamation marks, no error codes.

Accessibility

  • Every control sits in a Field — that gives it a label, and links hint and error through aria-describedby.
  • Errors are text next to the field. Colour is the second signal, never the only one.
  • On a failed submit focus jumps to the first invalid field, so keyboard and screen-reader users land on the problem.
  • Tab order follows the visual order — never reorder fields with CSS.
  • Use autocomplete and the right type for personal data.