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
<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 fields
space-y-5on 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.
- 1While typing, before the first submit
No errors. Don’t shout at people who haven’t finished.
- 2On submit with problems
Every invalid field shows its message at once, and focus moves to the first one.
- 3While fixing
Each message disappears live as soon as its field is valid — no need to submit again to see progress.
- 4After 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 of | Write |
|---|---|
| Invalid input | Enter a valid email address. |
| This field is required | Name is required. |
| Error: date | Pick 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 througharia-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
autocompleteand the righttypefor personal data.