Hacktron Design System

Form

Schema-validated fields, composed from Form and FormField.

Composing a form

A form is two components. Form owns validation and submission: a Zod schema plus a reactive state object, with no field UI of its own. FormField wraps one control with its label, optional description or hint, and validation error. Every field's name must match a key in the schema and state; that is what routes an error to the right FormField.

A basic form

<template>  <Form :schema="schema" :state="state" @submit="onSubmit">    <FormField label="Email" name="email">      <Input v-model="state.email" placeholder="you@example.com" class="w-full" />    </FormField>    <Button html-type="submit" class="self-start">Submit</Button>  </Form></template>

Field types

Every control, one schema

One seat, billed monthly.

Up to 20 seats, billed monthly.

<template>  <Form :schema="schema" :state="state" @submit="onSubmit">    <FormField label="Name" name="name">      <Input v-model="state.name" placeholder="Jane Doe" class="w-full" />    </FormField>    <FormField label="Bio" name="bio">      <Textarea v-model="state.bio" :rows="3" class="w-full" />    </FormField>    <FormField label="Role" name="role">      <Select v-model="state.role" :items="roleOptions" class="w-full" />    </FormField>    <FormField label="Seats" name="seats">      <InputNumber v-model="state.seats" :min="1" :step="1" class="w-32" />    </FormField>    <FormField label="Plan" name="plan">      <RadioGroup v-model="state.plan" :items="planOptions" />    </FormField>    <FormField name="notify">      <Switch v-model="state.notify" label="Email notifications" />    </FormField>    <Button html-type="submit">Submit</Button>  </Form></template>

Validating and submitting

  • The schema drives field-level errors, routed by name.
  • A trigger outside the <Form> element, such as a drawer or dialog footer, calls formRef.value?.validate() from a template ref. See the drawer example below.
  • Enter submits only when the form has a submit button (html-type="submit").
  • Form has no submitting state. The submit button's loading prop is toggled around the async handler.

Examples

Full page composition

A settings form inside a PageSection and a Card, with the submit button inside the form and its loading prop toggled around the async handler.

Profile

Your name and contact email.

<template>  <PageSection title="Profile" description="Your name and contact email.">    <Card>      <Form :schema="schema" :state="state" class="flex flex-col gap-4" @submit="onSubmit">        <FormField label="Display name" name="displayName">          <Input v-model="state.displayName" class="w-full" />        </FormField>        <FormField label="Email" name="email">          <Input v-model="state.email" class="w-full" />        </FormField>        <Button html-type="submit" :loading="saving" class="self-start">Save</Button>      </Form>    </Card>  </PageSection></template>

The footer stays fixed while the body scrolls, so its Save button sits outside the Form element and calls formRef.value?.validate().

<template>  <Drawer v-model:open="open" title="New automation">    <Form ref="formRef" :schema="schema" :state="state" class="flex flex-col gap-4">      <FormField label="Title" name="title">        <Input v-model="state.title" class="w-full" />      </FormField>      <FormField label="Description" name="description">        <Textarea v-model="state.description" :rows="3" class="w-full" />      </FormField>      <FormField label="Trigger" name="trigger">        <Select v-model="state.trigger" :items="triggerOptions" class="w-full" />      </FormField>      <FormField name="enabled">        <Switch v-model="state.enabled" label="Enabled" />      </FormField>    </Form>    <template #footer>      <Button variant="outline" @click="open = false">Cancel</Button>      <Button :loading="saving" @click="onSave">Save</Button>    </template>  </Drawer></template>

Props

Name Type Default
schemaZodSchema | ...-
stateobject-
...propsUForm props (validate, validateOn, disabled, name, nested, transform, id, ...)-

Slots

Name Description
defaultForm fields, scoped with `errors` and `loading`.

Guidelines

Do

  • Give every field a name that matches its key in schema and state.
  • Wrap every control in a FormField, including a Switch or Checkbox that carries its own label.
  • Include a submit button with html-type="submit" so that Enter submits the form.
  • Call formRef.value?.validate() from a template ref when the trigger sits outside the <Form> element.

Don't

  • Don't validate with watch or per-field checks when the schema already expresses the rule.
  • Don't track a submitting state in Form; toggle the submit button's loading prop instead.
  • Don't build a form from bare controls without Form; validation and error routing only work through it.