Skip to content

Forms

Forms collect user input. Aurora gives you the building blocks (label, field, message) and the field components (TextInput, Combobox, DatePicker, etc.) — this page covers how to put them together so every form across the product feels the same.

This demo is real. Click into a field, leave it blank, tab out — the error appears on blur. Errors clear as you fix them. Submit only fires once everything passes the schema.

Anatomy

A form field is four pieces that always travel together. Aurora ships them as small wrappers around vee-validate — the validation library does the wiring, the wrappers do the styling.

PieceWhat it does
FormFieldBinds a field to vee-validate. Exposes componentField slot props (value, onChange, error state).
FormItemLayout wrapper. layout="stacked" (default — label above) or layout="inline" (label beside, for checkboxes/toggles).
FormLabelThe label text. Reads required from the field schema and renders the red asterisk automatically.
FormMessageRenders inline error / helper text. Reads the validation error from FormField automatically.
html
<FormField v-slot="{ componentField }" name="email">
  <FormItem>
    <FormLabel>Email</FormLabel>
    <TextInput v-bind="componentField" placeholder="you@example.com" />
    <FormMessage />
  </FormItem>
</FormField>

That structure — FormField → FormItem → (FormLabel, the input, FormMessage) — is the same for every field type. Swap TextInput for Combobox, DatePicker, Checkbox, etc. as needed.

Layout patterns

Single column

Short, focused flows — onboarding, login, quick-edit panels. Keeps the user's eye on one decision at a time. Default to single column whenever the form has fewer than ~4 fields.

html
<form class="flex flex-col gap-4 max-w-md">
  <FormField v-slot="{ componentField }" name="username">
    <FormItem>
      <FormLabel>Username</FormLabel>
      <TextInput v-bind="componentField" />
      <FormMessage />
    </FormItem>
  </FormField>
  <!-- … -->
</form>

Two-column grid

Forms with 4+ fields where pairs of short fields can sit side by side (first/last name, city/postal, year/quarter). Wider fields (address, notes) span the full row.

html
<form class="grid grid-cols-2 gap-4 max-w-2xl">
  <FormField v-slot="{ componentField }" name="firstName">
    <FormItem><FormLabel>First name</FormLabel><TextInput v-bind="componentField" /></FormItem>
  </FormField>
  <FormField v-slot="{ componentField }" name="lastName">
    <FormItem><FormLabel>Last name</FormLabel><TextInput v-bind="componentField" /></FormItem>
  </FormField>
  <!-- Address spans both columns -->
  <FormField v-slot="{ componentField }" name="address">
    <FormItem class="col-span-2"><FormLabel>Address</FormLabel><TextInput v-bind="componentField" /></FormItem>
  </FormField>
</form>

Inline (checkbox / toggle)

For boolean fields, the label sits to the right of the control. Use layout="inline" on FormItem.

html
<FormField v-slot="{ componentField }" name="terms" type="checkbox" :value="true" :unchecked-value="false">
  <FormItem layout="inline">
    <Checkbox v-bind="componentField" />
    <FormLabel>I agree to the terms and conditions</FormLabel>
    <FormMessage />
  </FormItem>
</FormField>

Composition rules

  • Label above the field, never inside. Placeholder text disappears on focus and is invisible to screen readers — it is not a label substitute.
  • Mark required, not optional. A red asterisk on required fields. If most fields are required, flip it: mark the optional ones with a "(optional)" hint to reduce visual noise.
  • Validate inline, on blur. Show the error as soon as the field loses focus — don't make the user submit and discover ten errors at once.
  • One primary button per form. Cancel / Back use secondary or tertiary. Two primaries side-by-side make the dominant action ambiguous.
  • Cap dense layouts at 3 columns. Even in wide containers. Short fields (year, percentage, code) can share a row; longer fields (name, address, notes) span ≥ 2 columns.
  • Don't use placeholders as labels. They vanish on focus.
  • Don't validate only on submit. Surprises are not a feature.
  • Don't stack two primary buttons. Pick one.
  • Don't exceed a 3-column grid. Even on a 4K monitor.

Field states

Every input ships with the same set of validation states — default, error, warning, success, missing, disabled, readonly. You usually don't set them by hand: vee-validate's componentField slot prop forwards the error state from the schema, and the input renders the red border + icon + message automatically.

When you do need to set a state manually (e.g. async server validation, "this field needs review"):

html
<TextInput v-bind="componentField" warning />
<FormMessage warning message="Looks unusual — please double-check." />

FormMessage accepts error, warning, success, missing flags and a message prop for non-vee-validate cases.

vee-validate + Zod

Aurora's form components are designed to compose with vee-validate and Zod. The schema is the single source of truth — required-ness, types, and error messages all flow from it.

ts
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import type { DateValue } from '@scaler-tech/aurora/date-picker'

const schema = z.object({
    firstName: z.string().nonempty(),
    lastName: z.string().nonempty(),
    email: z.string().email(),
    country: z.string().nonempty(),
    birthDate: z.custom<DateValue>().transform(v => v.toString()),
    terms: z.coerce.boolean().refine(v => v, {
        message: 'You must accept the terms and conditions',
    }),
})

const { handleSubmit, controlledValues } = useForm({
    validationSchema: toTypedSchema(schema),
})

const onSubmit = handleSubmit((values) => {
    api.post('/account', values)
})

A few aurora-specific notes:

  • DatePicker v-model is a DateValue object, not a string. Call .toString() on submit (or transform on the schema, like above) to get an ISO YYYY-MM-DD.
  • Combobox for object values. When you want the full option object back (not just the value), pass the object as :value on ComboboxItem and call componentField.onChange(option) from @select. See the Combobox docs for the recipe.

Submit / cancel buttons

Place the action row at the bottom-right of the form on desktop, full-width-stacked on mobile. Primary first (right), secondary after (left). The primary action carries the verb-noun label that names what the form does ("Save changes", "Create asset", "Submit report") — never just "OK" or "Submit".

html
<div class="flex justify-end gap-2 mt-6">
  <Button variant="tertiary" @click="cancel">Cancel</Button>
  <Button type="submit">Save changes</Button>
</div>

For long forms, sticky the action row to the bottom of the panel so the buttons stay reachable without scrolling.