Usage
Use the Form component to validate form data using any validation library supporting Standard Schema such as Valibot, Zod, Regle, Yup, Joi or Superstruct or your own validation logic.
It works with the FormField component to display error messages around form elements automatically.
Schema validation
It requires two props:
state- a reactive object holding the form's state.schema- any Standard Schema or Superstruct.
Custom validation
Use the validate prop to apply your own validation logic.
The validation function must return a list of errors with the following attributes:
message- the error message to display.name- thenameof theFormFieldto send the error to.
schema prop to handle complex use cases.Error reporting
Errors are matched to the corresponding FormField using its name prop. An error on the email field is shown by <FormField name="email">.
Nested fields are matched using dot notation. A schema like { user: z.object({ email: z.string() }) } will be applied to <FormField name="user.email">.
tags.0, tags.1) and won't match <FormField name="tags"> by name alone. Use the error-pattern prop with a regular expression like /^tags\..+/ to capture them. This is especially useful for components like InputTags.Input events
The Form component automatically triggers validation when an input emits an input, change, or blur event.
- Validation on
inputoccurs as you type. - Validation on
changeoccurs when you commit to a value. - Validation on
blurhappens when an input loses focus.
You can control when validation happens this using the validate-on prop.
useFormField composable to implement this inside your own components.change event these inputs emit is a syntheticEvent, and its target is null — it carries no value. @change="e => e.target.value" throws.It exists so you can react to a commit; it is not how the value travels, and it is not how the Form learns about it either — validation is driven by a separate internal call. Read the value from @update:model-value instead, or from v-model:<template>
<B24Select v-model="value" :items="items" @update:model-value="onCommit" />
</template>
change without a native event behind it — Select, SelectMenu, InputMenu, InputDate, InputTime, InputTags, InputNumber, InputRating, PinInput, Listbox, Range, Checkbox, CheckboxGroup, RadioGroup, Switch and FileUpload. Input and Textarea forward the browser's own change event, so their target is the real element.Error event
You can listen to the @error event to handle errors. This event is triggered when the form is submitted and contains an array of FormError objects with the following fields:
id- the input'sid.name- thenameof theFormFieldmessage- the error message to display.
Here's an example that focuses the first input element with an error after the form is submitted:
HTML5 validation
When calling form.submit() programmatically, the Form component automatically triggers native HTML5 validation before submission.
Nesting forms
Use the nested prop to nest multiple Form components and link their validation functions. In this case, validating the parent form will automatically validate all the other forms inside it.
Nested forms directly inherit their parent's state, so you don't need to define a separate state for them. You can use the name prop to target a nested attribute within the parent's state.
It can be used to dynamically add fields based on user's input:
Or to validate list inputs:
Examples
Record edit section
A common record-edit pattern (UF placement, slider context): a titled section with vertical-label fields, a two-column row for amount + currency, a nested "Client" sub-section, and a series of additional fields. Built entirely from B24Form, B24FormField, B24Input, B24Select, B24InputNumber, B24InputDate, B24Popover and B24Calendar — no custom components.
The full field set: Stage (B24Select), Amount and currency (B24InputNumber + B24Select in a two-column row), a "Client" group (Company + Contact B24Inputs and an "Add participant" link), then Salutation (B24Select), Last name and First name (B24Input), Service type (B24Select) and Scheduled date (B24InputDate with a dropdown B24Calendar in a B24Popover).
The "Client" sub-section is just a <div> with a label above and a bordered container (rounded-md border ... p-3 sm:p-4 space-y-4) wrapping nested B24FormFields — role="group" + aria-labelledby associate the label with the group. The two-column "Amount and currency" row uses grid-cols-1 sm:grid-cols-[1fr_auto] so the currency drops below the amount on narrow viewports. B24InputDate binds an @internationalized/date value (not a native Date), so the schema types that field loosely.
Build a record-edit form section (slider / UF placement edit panel).
Lean on the b24-ui-nuxt skill (see references/guidelines/forms.md → "Record-edit form pattern") to build a record-edit form section — the titled edit panel used in Bitrix24 sliders and UF placements. Assemble it only from stock primitives (B24Form, B24FormField, B24Input, B24Select, B24InputNumber, B24InputDate, B24Popover, B24Calendar) under a single :schema; no custom components.
Before writing any code, ask me for the missing context — don't assume:
- Which record is being edited (deal, lead, order, ticket, contact…)? That decides the section title, the field set and the validation rules.
- Which fields belong in the section, in what order, and which are required? Group related ones (e.g. a "Client" sub-section) and flag any two-column rows (such as amount + currency).
- For each field, which control fits: enum →
B24Select(list the options), free text →B24Input, number →B24InputNumber, date →B24InputDatewith a dropdownB24Calendar. - The header affordance: a title plus a single icon-only action on the right — confirm the icon and its
aria-label. - Locale, the default values used by the reset action, and what submit should do (toast, API call, close the slider).
Once those answers are in:
- Build the labelled sub-section as a
<div role="group" :aria-labelledby="...">whose id comes fromuseId()(b24ui has no fieldset primitive). - For a full-width
B24Select, set bothclass(the trigger) and:b24ui="{ root: 'w-full' }"(the wrapper);B24Input/B24InputNumberneed onlyclass="w-full". - Type the date field loosely in the schema (
z.any().optional()) —B24InputDatebinds an@internationalized/datevalue, not a nativeDate— and pair the input with aB24Calendarin its#trailingB24Popover, both sharing the samev-model. - Use
color="air-tertiary-no-accent"for icon/link-style buttons (the header action and "Add participant") — there is novariantprop onB24Button.
Keep all copy in the requested locale.
Task form layout
Real-world example: a Bitrix24-style task form assembled from standard components only — Input for the title, Editor (with a minimal toolbar row) for the description, Card, Avatar, and InputDate for the responsible-persons block, and a wrap row of Button actions. Single-column layout throughout — no custom CSS beyond component props.
Build a Bitrix24-style task form using standard b24ui components.
Build a Bitrix24-style task form layout using only standard b24ui components (single column, no custom CSS beyond component props).
- Title:
B24Inputsize="xl"no-border, bold via:b24ui="{ base: 'font-(--ui-font-weight-semi-bold)' }" - Editor card:
B24Cardwithb24ui.body='p-0'containingB24Editor(content-type="markdown",min-h-48 px-4 py-3). Inside the editor's default slot, render a toolbar row (flex items-center gap-1 px-2 py-1.5 border-b): attachmentB24Buttonon the left,B24EditorToolbarwithmention/bulletList/orderedListin the middle, expandB24Button(GoToLIcon) pushed toml-auto - Responsible persons card:
B24Cardb24ui.body='p-0', body is adivide-ydiv with threepx-5 py-3rows — Creator (B24Avatar+ name), Assignee (B24Avatar+ name), Deadline (B24InputDatesize="sm"no-borderwithB24Popover+B24Calendarin the#trailingslot, both sharing the sameshallowRef<CalendarDate | undefined>) - Watchers card:
#headerwith label and+B24Button; body showsB24Avataricons - Action buttons:
flex flex-wrap gap-2row of 17B24Buttonsize="sm"— each item has anactiveflag; active →color="air-secondary-accent-2", inactive →color="air-secondary-no-accent". Buttons: Results, Files, Checklists, Project, Co-executors, Observers, Flow, Tags, Reminders, CRM elements, Parent task, Subtasks, Linked tasks, Gantt, Timeline planning, Time tracking, Custom fields - Footer:
flex gap-2 justify-endwith Save (air-primary) and Cancel (air-tertiary) - State:
titleanddescriptionasref;deadlineasshallowRef<CalendarDate | undefined>(from@internationalized/date—B24InputDatedoes not accept nativeDate);toolbarItemsas a plainconst(no reactive deps); action list typed as{ label: string, icon: IconComponent, active?: boolean }[]
API
Props
Slots
Emits
Expose
You can access the typed component instance using useTemplateRef.
<script setup lang="ts">
const form = useTemplateRef('form')
</script>
<template>
<B24Form ref="form" />
</template>
This will give you access to the following:
Theme
export default {
base: ''
}