Form
A neo-brutalist style form system built on vee-validate, providing composable sub-components for structured form layout.
Demo
Installation
pnpm dlx brutx-vue@latest add formAdditional dependencies required:
pnpm add vee-validate @vee-validate/zod zodUsage
<script setup>
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from 'brutx-ui-vue/form'
import { Input, Button } from 'brutx-ui-vue'
const schema = toTypedSchema(z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
}))
function onSubmit(values) {
console.log('Form submitted:', values)
}
</script>
<template>
<Form :validation-schema="schema" @submit="onSubmit">
<FormField name="username" v-slot="{ componentField }">
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input v-bind="componentField" placeholder="Enter username" />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField name="email" v-slot="{ componentField }">
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input v-bind="componentField" type="email" placeholder="you@example.com" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<Button type="submit" variant="primary">Submit</Button>
</Form>
</template>FormWizard Multi-step Form
A wizard-style form built on the Stepper component, supporting step validation, linear navigation, and custom step content.
<script setup>
import { ref } from 'vue'
import { FormWizard } from 'brutx-ui-vue/form'
import type { FormStep } from 'brutx-ui-vue/form'
import { Input } from 'brutx-ui-vue'
const values = ref({})
const steps = [
{ id: 'personal', title: 'Personal Info' },
{ id: 'address', title: 'Address' },
{ id: 'review', title: 'Review' },
]
function onComplete(finalValues) {
console.log('Submitted:', finalValues)
}
</script>
<template>
<FormWizard
v-model="values"
:steps="steps"
@complete="onComplete"
>
<template #step-personal>
<Input v-model="values.name" placeholder="Name" />
</template>
<template #step-address>
<Input v-model="values.address" placeholder="Address" />
</template>
<template #step-review>
<p>Review info: {{ values }}</p>
</template>
</FormWizard>
</template>FormConditional Conditional Fields
Use a predicate to control whether a group is mounted. This example closes over caller-managed reactive values and passes initial values to Form:
<script setup>
import { ref } from 'vue'
import { Form, FormConditional, FormField } from 'brutx-ui-vue/form'
const values = ref({ type: 'company' })
const showCompany = () => values.value.type === 'company'
const showPersonal = () => values.value.type === 'personal'
</script>
<template>
<Form :initial-values="values">
<FormField name="type" />
<FormConditional :when="showCompany">
<FormField name="companyName" />
<FormField name="taxId" />
</FormConditional>
<FormConditional :when="showPersonal">
<FormField name="idNumber" />
</FormConditional>
</Form>
</template>Binding FormControl to an Input
FormControl requires exactly one root control. Its scoped slot provides the generated ID, description and invalid-state attributes, and the shared form size:
<FormControl v-slot="{ id, ariaDescribedby, ariaInvalid, size }">
<Input :id="id" :size="size" :aria-describedby="ariaDescribedby" :aria-invalid="ariaInvalid" />
</FormControl>Sub-components
| Component | Description |
|---|---|
Form | Root form component, integrates with vee-validate |
FormField | Field wrapper that connects form state and provides field context |
FormItem | Layout container for label, control, and message; generates unique IDs and applies form layout settings |
FormLabel | Label with error state support, injects field context |
FormControl | Input control wrapper that places the control in the form grid and provides accessibility attributes and size via slot |
FormDescription | Helper text below the input |
FormMessage | Validation error message, injects field context |
FormWizard | Multi-step wizard-style form |
FormConditional | Dynamically shows/hides field groups based on form values |
Data Types
FormFieldContext
FormField provides FormFieldContext to child components via provide/inject:
interface FormFieldContext {
name: ComputedRef<string>
error: Ref<string | undefined>
value: Ref<unknown>
setValue: (value: unknown) => void
setError: (message: string | undefined) => void
}FormItemContext
FormItem provides FormItemContext to child components via provide/inject:
interface FormItemContext {
id: string
formItemId: string
formDescriptionId: string
formMessageId: string
}FormControl, FormLabel, and FormMessage all inject FormFieldContext and FormItemContext, providing direct access to field values and error states.
FormStep
interface FormStep {
id: string
title: string
description?: string
icon?: Component
validator?: (values: Record<string, unknown>) => ValidationResult
optional?: boolean
}ValidationResult
interface ValidationResult {
valid: boolean
errors: Record<string, string>
}Composables
useFormWizard
Access the wizard context within FormWizard child components:
const {
currentStep, // Ref<number> - Current step
steps, // ComputedRef<FormStep[]> - Step configuration
values, // ComputedRef<Record<string, unknown>> - Form data
updateValues, // (values: Record<string, unknown>) => void - Update form data
nextStep, // () => void - Go to next step
previousStep, // () => void - Go to previous step
goToStep, // (step: number) => void - Jump to a step
complete, // () => void - Complete the form
getStepErrors, // (step: number) => Record<string, string> | undefined - Get errors for a specific step
isFirstStep, // ComputedRef<boolean>
isLastStep, // ComputedRef<boolean>
canGoNext, // ComputedRef<boolean>
} = useFormWizard()Programmatic Control
Access the Form component instance via ref to call the following methods:
<script setup>
import { ref } from 'vue'
import { Form } from 'brutx-ui-vue/form'
const formRef = ref(null)
async function handleSubmit() {
const isValid = await formRef.value?.validate()
if (isValid) {
// Submit form
}
}
function handleReset() {
formRef.value?.resetFields()
}
</script>
<template>
<Form ref="formRef" :scroll-to-error="true">
<!-- Form fields -->
</Form>
</template>API Reference
Form
| Name | Type | Default | Description |
|---|---|---|---|
inline | boolean | undefined | false | Arranges form items in a wrapping horizontal layout. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/form/Form.vue | |||
labelPosition | "right" | "left" | "top" | undefined | "right" | left and right place labels to the left of controls with the corresponding alignment; top places labels above controls. |
DetailsDeclared type"right" | "left" | "top" | undefined Literal values
Default value details
Sourcepackages/ui/src/components/form/Form.vue | |||
labelWidth | string | number | undefined | undefined | The label-column width in horizontal label layouts. Numbers are pixels and strings are CSS widths; top layouts do not use this value. |
DetailsDeclared typestring | number | undefined Default value details
Sourcepackages/ui/src/components/form/Form.vue | |||
scrollToError | boolean | undefined | false | Smoothly scrolls to the first invalid field when the instance validate() method fails. Native submission handling does not use this option. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/form/Form.vue | |||
size | "default" | "lg" | "sm" | undefined | "default" | Provides a shared size for item spacing, labels, descriptions, and errors, and exposes size to controls through the FormControl slot. |
DetailsDeclared type"default" | "lg" | "sm" | undefined Literal values
Default value details
Sourcepackages/ui/src/components/form/Form.vue | |||
class | string | undefined | undefined | Custom CSS classes merged onto the native form root. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/form/Form.vue | |||
initialValues | Record<string, unknown> | undefined | undefined | Initial field values passed to vee-validate. A changed prop reference resets the form with the new values while the form is pristine. |
DetailsDeclared typeRecord<string, unknown> | undefined Default value details
Sourcepackages/ui/src/components/form/Form.vue | |||
validationSchema | unknown | undefined | The reactive validation schema passed to vee-validate, using supported rules or a converted Zod schema. |
DetailsDeclared typeunknown Default value details
Sourcepackages/ui/src/components/form/Form.vue | |||
| Name | Signature or type | Description |
|---|---|---|
@submit | [values: Record<string, unknown>] | Emits field values after form submission passes vee-validate validation. |
DetailsDeclared type[values: Record<string, unknown>] Sourcepackages/ui/src/components/form/Form.vue | ||
| Name | Signature or type | Description |
|---|---|---|
#default | {} | Form fields, descriptions, and submission controls. Descendants can access the validation and layout context provided by Form. |
DetailsDeclared type{}Sourcepackages/ui/src/components/form/Form.vue | ||
| Name | Signature or type | Description |
|---|---|---|
validate | () => Promise<boolean> | Validates all fields and asynchronously returns their validity. On failure, scrolls to the first invalid field when scrollToError is enabled. |
DetailsDeclared type() => Promise<boolean> Sourcepackages/ui/src/components/form/Form.vue | ||
validateField | (field: string) => Promise<boolean> | Validates one field by name and asynchronously returns whether it is valid. |
DetailsDeclared type(field: string) => Promise<boolean> Sourcepackages/ui/src/components/form/Form.vue | ||
resetFields | () => void | Calls vee-validate resetForm to restore fields and form state to their initial state. |
DetailsDeclared type() => void Sourcepackages/ui/src/components/form/Form.vue | ||
clearValidate | (fields?: string[] | undefined) => void | Clears validation errors for the specified fields. Omission or an empty array clears all errors while preserving current field values. |
DetailsDeclared type(fields?: string[] | undefined) => void Sourcepackages/ui/src/components/form/Form.vue | ||
scrollToField | (field: string) => void | Finds a form control by name and smoothly scrolls to its top. Radio groups use the first control; no scrolling occurs when the field is absent. |
DetailsDeclared type(field: string) => void Sourcepackages/ui/src/components/form/Form.vue | ||
FormConditional
| Name | Type | Default | Description |
|---|---|---|---|
whenRequired | (values: Record<string, unknown>) => boolean | — | The predicate deciding whether conditional content is mounted. Use it inside Form and prefer a stable named function reference.
|
DetailsDeclared type(values: Record<string, unknown>) => boolean Default value details
Sourcepackages/ui/src/components/form/FormConditional.vue | |||
class | string | undefined | undefined | CSS classes merged onto the content wrapper div when the condition is true. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/form/FormConditional.vue | |||
| Name | Signature or type | Description |
|---|---|---|
#default | {} | Fields or other content mounted when the condition is true. The entire region is unmounted when it becomes false. |
DetailsDeclared type{}Sourcepackages/ui/src/components/form/FormConditional.vue | ||
FormControl
| Name | Type | Default | Description |
|---|---|---|---|
class | string | undefined | — | CSS classes merged with form-control layout styles and applied through as-child to the single root control in the default slot. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/form/FormControl.vue | |||
| Name | Signature or type | Description |
|---|---|---|
#default | { ariaDescribedby: string | undefined; ariaInvalid: true | undefined; size: FormLayoutSize | undefined; class: string; id: string | undefined; } | Contains one root control. Provides the FormItem-linked id, class, ariaDescribedby, ariaInvalid, and Form size for explicit binding. The description relationship includes the error message when meaningful error text is present. |
DetailsDeclared type{ ariaDescribedby: string | undefined; ariaInvalid: true | undefined; size: FormLayoutSize | undefined; class: string; id: string | undefined; }Referenced types FormLayoutSize Type definition — FormLayoutSizeexport type FormLayoutSize = 'sm' | 'default' | 'lg' Sourcepackages/ui/src/components/form/FormControl.vue | ||
FormDescription
| Name | Type | Default | Description |
|---|---|---|---|
class | string | undefined | — | CSS classes merged onto the field description paragraph, which uses the description ID provided by FormItem. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/form/FormDescription.vue | |||
| Name | Signature or type | Description |
|---|---|---|
#default | {} | Supporting guidance for the field. The description paragraph is not rendered when this slot is omitted. |
DetailsDeclared type{}Sourcepackages/ui/src/components/form/FormDescription.vue | ||
FormField
| Name | Type | Default | Description |
|---|---|---|---|
nameRequired | string | — | The field name registered with vee-validate. Keep it stable during the component lifetime; changing it requires handling retained values and errors for the previous name. |
DetailsDeclared typestring Default value details
Sourcepackages/ui/src/components/form/FormField.vue | |||
| Name | Signature or type | Description |
|---|---|---|
#default | { componentField: { name: string; modelValue: unknown; 'onUpdate:modelValue': (e: unknown, shouldValidate?: boolean | undefined) => void; onFocusout: (event: FocusEvent) => void; }; } | Field content, typically FormItem. Provides componentField for v-bind on the control, including the field name, current value, value updates, and focusout validation. |
DetailsDeclared type{ componentField: { name: string; modelValue: unknown; 'onUpdate:modelValue': (e: unknown, shouldValidate?: boolean | undefined) => void; onFocusout: (event: FocusEvent) => void; }; }Sourcepackages/ui/src/components/form/FormField.vue | ||
FormItem
| Name | Type | Default | Description |
|---|---|---|---|
class | string | undefined | — | CSS classes merged onto the field layout container, which follows the Form label position, width, and size. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/form/FormItem.vue | |||
| Name | Signature or type | Description |
|---|---|---|
#default | {} | The field label, control, description, and error message, sharing the IDs generated by this FormItem. |
DetailsDeclared type{}Sourcepackages/ui/src/components/form/FormItem.vue | ||
FormLabel
| Name | Type | Default | Description |
|---|---|---|---|
class | string | undefined | — | CSS classes merged onto the field label. Meaningful error text automatically applies the error color. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/form/FormLabel.vue | |||
| Name | Signature or type | Description |
|---|---|---|
#default | {} | The field label content, linked through for to the FormItem control ID. |
DetailsDeclared type{}Sourcepackages/ui/src/components/form/FormLabel.vue | ||
FormMessage
| Name | Type | Default | Description |
|---|---|---|---|
class | string | undefined | — | CSS classes merged onto the validation error paragraph. It renders with role="alert" only when the field error is non-empty after trimming. |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/form/FormMessage.vue | |||
FormWizard
| Name | Type | Default | Description |
|---|---|---|---|
stepsRequired | FormStep[] | — | The step configuration list with stable IDs, titles, and optional validators. Each ID determines its step-{id} slot name. |
DetailsDeclared typeFormStep[] Referenced types FormStep Type definition — FormStepexport interface FormStep {
id: string
title: string
description?: string
icon?: Component
/** 入参为全量 values(跨步骤共享的扁平字段集合),约定只返回当前步骤相关字段的错误 */
validator?: (values: Record<string, unknown>) => ValidationResult
/** 线性导航下可跳过(不要求完成即可前进),但不豁免 validator */
optional?: boolean
}Default value details
Sourcepackages/ui/src/components/form/form-wizard-types.ts | |||
modelValue | Record<string, unknown> | undefined | { }Factory evaluated per instance | The flat field record shared across steps and controlled through v-model. useFormWizard().updateValues() emits updates after shallowly merging field patches. |
DetailsDeclared typeRecord<string, unknown> | undefined Default value details
Sourcepackages/ui/src/components/form/form-wizard-types.ts | |||
initialStep | number | undefined | 0 | The initial zero-based step index, clamped to the valid steps range at creation. |
DetailsDeclared typenumber | undefined Default value details
Sourcepackages/ui/src/components/form/form-wizard-types.ts | |||
validateOnNext | boolean | undefined | true | Validates the current step when advancing, and revalidates all steps with validators when completing, including optional steps. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/form/form-wizard-types.ts | |||
showIndicator | boolean | undefined | true | Shows the top step indicator, which users can use to request navigation. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/form/form-wizard-types.ts | |||
linear | boolean | undefined | true | Requires preceding required steps to be completed before forward navigation. Optional steps may be skipped; backward navigation is unrestricted. |
DetailsDeclared typeboolean | undefined Literal values
Default value details
Sourcepackages/ui/src/components/form/form-wizard-types.ts | |||
class | string | undefined | undefined | CSS classes merged onto the wizard root container with role="form". |
DetailsDeclared typestring | undefined Default value details
Sourcepackages/ui/src/components/form/form-wizard-types.ts | |||
| Name | Signature or type | Description |
|---|---|---|
@update:modelValue | [values: Record<string, unknown>] | Emits the complete field record after shallowly merging a field update made through the wizard context. |
DetailsDeclared type[values: Record<string, unknown>] Sourcepackages/ui/src/components/form/FormWizard.vue | ||
@complete | [values: Record<string, unknown>] | Emits all form data when the completion action passes the enabled step validation. |
DetailsDeclared type[values: Record<string, unknown>] Sourcepackages/ui/src/components/form/FormWizard.vue | ||
@step-change | [step: number, previousStep: number] | Emits the target and previous zero-based indices on navigation or when completion validation returns to a failing step. |
DetailsDeclared type[step: number, previousStep: number] Sourcepackages/ui/src/components/form/FormWizard.vue | ||
@validation-error | [step: number, errors: Record<string, string>] | Emits the step index and field error record when a step validator returns an invalid result. |
DetailsDeclared type[step: number, errors: Record<string, string>] Sourcepackages/ui/src/components/form/FormWizard.vue | ||
@navigation-blocked | [targetStep: number, blockedStep: number] | Emits the target and blocking indices when linear navigation encounters an incomplete required preceding step. |
| Name | Signature or type | Description |
|---|---|---|
#step-{id} | { step: FormStep; index: number } | A dynamic slot named after the step ID, receiving its configuration and zero-based index. All step content remains mounted, with v-show selecting the current panel. |
DetailsDeclared type{ step: FormStep; index: number }Sourcepackages/ui/src/components/form/FormWizard.vue | ||
Accessibility
Form Structure
FormItemautomatically generates unique IDs, ensuring correct associations betweenFormLabel,FormControl,FormDescription, andFormMessage.FormLabelis associated with the corresponding input control via theforattribute.FormControlprovidesid,aria-describedby, andaria-invalidattributes via a scoped slot, which should be bound to the inner input control to ensure accessibility.
Validation Errors
- On validation failure,
FormLabelautomatically adds thetext-brutal-destructivestyle. FormMessageusesrole="alert"to ensure screen readers announce error messages promptly.- The
aria-invalidattribute on the input control is automatically set totrue.
FormWizard Navigation
- The step indicator uses
role="tablist"androle="tab"semantics. - Supports keyboard navigation (Tab, Enter, Space).
- When
linearmode is enabled, blocked steps are identified witharia-disabled.
FAQ
Q: Form validation is not triggered on submit. What's wrong?
A: Make sure you have correctly installed and configured the vee-validate and @vee-validate/zod dependencies, and that validationSchema is passed to the Form component. Additionally, each field that needs validation must be wrapped in a FormField, and the FormField's name attribute must match the field name in the schema.
Q: How do I skip validation for certain steps in FormWizard?
A: Set optional: true in the steps configuration to mark a step as optional. Optional steps do not trigger validation when advancing. For finer control, you can customize the validation logic in the step's validator function and return { valid: true, errors: {} } to skip validation conditionally.
Q: Why isn't the FormConditional condition function working?
A: The when function of FormConditional receives the current form's complete value object as its argument. Make sure the field names are spelled correctly and the form values have been properly initialized via v-model or initialValues. If the condition depends on a field that hasn't been rendered yet or whose value is undefined, the condition evaluation may return unexpected results.