Skip to main content

Overview

SaaS Starter Vue provides a complete form system with validation support. Forms are built using vee-validate for validation logic and zod for schema definition, integrated with Inertia.js form handling.

Form Validation Stack

  • vee-validate - Form validation library
  • @vee-validate/zod - Zod integration for vee-validate
  • zod - TypeScript-first schema validation
  • Inertia.js useForm - Server-side validation and form state

Basic Form Example

Here’s a simple login form using Inertia.js form handling:
<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import InputError from '@/components/InputError.vue'

defineProps<{
  canResetPassword: boolean
}>()
</script>

<template>
  <Form
    v-bind="store()"
    :reset-on-success="['password']"
    v-slot="{ errors, processing }"
    class="flex flex-col gap-6"
  >
    <div class="grid gap-2">
      <Label for="email">Email address</Label>
      <Input
        id="email"
        type="email"
        name="email"
        required
        autofocus
        autocomplete="email"
        placeholder="[email protected]"
      />
      <InputError :message="errors.email" />
    </div>

    <div class="grid gap-2">
      <Label for="password">Password</Label>
      <Input
        id="password"
        type="password"
        name="password"
        required
        autocomplete="current-password"
        placeholder="Password"
      />
      <InputError :message="errors.password" />
    </div>

    <Button type="submit" :disabled="processing">
      Log in
    </Button>
  </Form>
</template>

Input Component

The Input component supports all standard HTML input attributes:
<script setup lang="ts">
import { Input } from '@/components/ui/input'
</script>

<template>
  <Input
    v-model="value"
    type="text"
    placeholder="Enter text..."
    :disabled="false"
  />
</template>

Input Props

  • modelValue - v-model binding
  • type - Input type (text, email, password, etc.)
  • placeholder - Placeholder text
  • disabled - Disabled state
  • class - Additional CSS classes

Input States

<template>
  <!-- Normal state -->
  <Input placeholder="Normal input" />
  
  <!-- Disabled state -->
  <Input placeholder="Disabled" disabled />
  
  <!-- Invalid state (via aria-invalid) -->
  <Input aria-invalid="true" placeholder="Invalid input" />
</template>

Textarea Component

For multi-line text input:
<script setup lang="ts">
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
</script>

<template>
  <div class="grid gap-2">
    <Label for="description">Description</Label>
    <Textarea
      id="description"
      v-model="description"
      placeholder="Enter description..."
      rows="4"
    />
  </div>
</template>

Select Component

Dropdown selection using reka-ui:
<script setup lang="ts">
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select'
</script>

<template>
  <Select v-model="selectedValue">
    <SelectTrigger>
      <SelectValue placeholder="Select an option" />
    </SelectTrigger>
    <SelectContent>
      <SelectGroup>
        <SelectLabel>Options</SelectLabel>
        <SelectItem value="option1">Option 1</SelectItem>
        <SelectItem value="option2">Option 2</SelectItem>
        <SelectItem value="option3">Option 3</SelectItem>
      </SelectGroup>
    </SelectContent>
  </Select>
</template>

Checkbox Component

For boolean selections:
<script setup lang="ts">
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
</script>

<template>
  <Label class="flex items-center space-x-3">
    <Checkbox id="remember" name="remember" />
    <span>Remember me</span>
  </Label>
</template>
From resources/js/pages/system/auth/Login.vue:82-87:
<Label for="remember" class="flex items-center space-x-3">
  <Checkbox id="remember" name="remember" :tabindex="3" />
  <span>Remember me</span>
</Label>

Switch Component

Toggle switches for on/off states:
<script setup lang="ts">
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { ref } from 'vue'

const isEnabled = ref(false)
</script>

<template>
  <div class="flex items-center space-x-2">
    <Switch id="notifications" v-model:checked="isEnabled" />
    <Label for="notifications">Enable notifications</Label>
  </div>
</template>

Radio Group

For selecting one option from multiple choices:
<script setup lang="ts">
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label'
</script>

<template>
  <RadioGroup v-model="selectedOption">
    <div class="flex items-center space-x-2">
      <RadioGroupItem id="option1" value="option1" />
      <Label for="option1">Option 1</Label>
    </div>
    <div class="flex items-center space-x-2">
      <RadioGroupItem id="option2" value="option2" />
      <Label for="option2">Option 2</Label>
    </div>
  </RadioGroup>
</template>

Form with Client Validation

Using vee-validate with zod schemas:
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

const schema = toTypedSchema(
  z.object({
    email: z.string().email('Invalid email address'),
    password: z.string().min(8, 'Password must be at least 8 characters'),
  })
)

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

const onSubmit = handleSubmit((values) => {
  console.log('Form submitted:', values)
})
</script>

<template>
  <form @submit="onSubmit" class="space-y-4">
    <div>
      <Label for="email">Email</Label>
      <Input id="email" name="email" type="email" />
      <p v-if="errors.email" class="text-sm text-destructive mt-1">
        {{ errors.email }}
      </p>
    </div>
    
    <div>
      <Label for="password">Password</Label>
      <Input id="password" name="password" type="password" />
      <p v-if="errors.password" class="text-sm text-destructive mt-1">
        {{ errors.password }}
      </p>
    </div>
    
    <Button type="submit">Submit</Button>
  </form>
</template>

Form with Inertia.js

Combining Inertia.js form handling with components:
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import InputError from '@/components/InputError.vue'

const form = useForm({
  company_name: '',
  owner_name: '',
  owner_email: '',
  password: '',
})

const submit = () => {
  form.post('/guest-register', {
    onFinish: () => form.reset('password'),
  })
}
</script>

<template>
  <form @submit.prevent="submit" class="flex flex-col gap-4">
    <div class="grid gap-2">
      <Label for="company_name">Company Name</Label>
      <Input
        id="company_name"
        v-model="form.company_name"
        type="text"
        required
        autofocus
      />
      <InputError :message="form.errors.company_name" />
    </div>

    <div class="grid gap-2">
      <Label for="owner_email">Email</Label>
      <Input
        id="owner_email"
        v-model="form.owner_email"
        type="email"
        required
      />
      <InputError :message="form.errors.owner_email" />
    </div>

    <Button type="submit" :disabled="form.processing">
      {{ form.processing ? 'Creating...' : 'Create Account' }}
    </Button>
  </form>
</template>

Label Component

Accessible form labels with proper associations:
<script setup lang="ts">
import { Label } from '@/components/ui/label'
</script>

<template>
  <Label for="input-id">Field Label</Label>
Labels automatically handle:
  • Click-to-focus behavior
  • ARIA associations
  • Disabled state styling
  • Screen reader support

Form Structure Components

For organized form layouts:
<script setup lang="ts">
import {
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
</script>

<template>
  <FormItem>
    <FormLabel>Username</FormLabel>
    <FormControl>
      <Input placeholder="Enter username" />
    </FormControl>
    <FormDescription>
      This is your public display name.
    </FormDescription>
    <FormMessage />
  </FormItem>
</template>

Input Error Display

The InputError component displays validation errors:
<script setup lang="ts">
import InputError from '@/components/InputError.vue'
</script>

<template>
  <InputError :message="errors.fieldName" />
</template>

File Upload

Handling file uploads with preview:
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from '@inertiajs/vue3'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

const form = useForm({
  file: null as File | null,
})

const fileInput = ref<HTMLInputElement | null>(null)
const preview = ref<string | null>(null)

const handleFileChange = (event: Event) => {
  const target = event.target as HTMLInputElement
  const file = target.files?.[0]
  
  if (!file) return
  
  form.file = file
  
  const reader = new FileReader()
  reader.onload = (e) => {
    preview.value = e.target?.result as string
  }
  reader.readAsDataURL(file)
}
</script>

<template>
  <div class="space-y-2">
    <Label for="file">Upload File</Label>
    <Input
      id="file"
      ref="fileInput"
      type="file"
      @change="handleFileChange"
    />
    <img v-if="preview" :src="preview" alt="Preview" class="mt-2 max-w-xs" />
  </div>
</template>

Best Practices

  1. Always use labels - Provide accessible labels for all form fields
  2. Show validation errors - Display clear error messages near the relevant field
  3. Disable on submit - Prevent double submissions by disabling the submit button
  4. Reset on success - Clear sensitive fields after successful submission
  5. Use appropriate input types - email, password, tel, etc. for better UX
  6. Add autocomplete - Help users fill forms faster
  7. Provide placeholder text - Give users context about expected input

Accessibility

All form components include:
  • Proper ARIA attributes
  • Keyboard navigation
  • Focus indicators
  • Error announcements
  • Label associations

Build docs developers (and LLMs) love