Skip to content

Field ​

One field of a form: parts around one control.

html
<Field field-id="latitude" required :messages="messagesFor('latitude')">
  <FieldLabel>Latitude</FieldLabel>
  <FieldHint>Decimal degrees, WGS 84.</FieldHint>
  <TextInput v-model="record.latitude" />
  <FieldMessages />
</Field>

Field owns the field's state (id, required, disabled, messages) and the layout; the parts and the control read that state from a context, so they know nothing about each other or about the layout. Where the messages come from and how fields are grouped is on the Forms page.

ts
import { Field, FieldLabel, FieldHint, FieldMessages } from '@scaler-tech/aurora/forms'
import type { FormFieldMessage } from '@scaler-tech/aurora/forms'
Decimal degrees, WGS 84.
Latitude must be between -90 and 90.
Latitude looks unusual for this country.
Side layout: label and hint left, control right.
Postal code format differs from the country default.
Postal code format differs from the country default.

Layout ​

Field is a CSS grid with named areas; every part places itself by name (label, hint, control, messages). The control area is FieldControl — the control plus whatever sits beside it that is not a control, such as an action button — or, without one, the one child that is not a part. Write the parts in any order; omit any you do not need (an empty area has no height).

html
<Field v-slot="{ componentField }" name="gross_floor_area" :messages="messagesFor('gross_floor_area')">
  <FieldLabel>Gross floor area</FieldLabel>
  <FieldControl>
    <NumberInput v-bind="componentField" suffix="m²" />
    <IconButton icon="clock" variant="tertiary" aria-label="History" @click="openHistory('gross_floor_area')" />
  </FieldControl>
  <FieldMessages />
</Field>
LayoutAreas
stacked (default)label / hint / control / messages, one column. A control with no label or hint above it sits flush at the top
side (≥ md)label / hint / messages stacked on the left, control on the right spanning the label and hint rows and vertically centred on them (no hint → centred on the label line). Messages are their own row, so they never move the control. One shared control column (minmax(0,16rem)), controls left-aligned, so consecutive fields share a left edge whatever the control is; the label column never shrinks below 8rem. Text-like controls fill the column (they are w-full), a toggle or a w-fit wrapper keeps its own width. Stacks below md
nonea plain block — no grid. Arrange the parts in your own markup; the context still wires everything

Inside a two-column FormSection every field shares the section's row tracks (subgrid), so label, control and messages rows line up across the two columns. A field without a hint next to one with a hint keeps its label right above its control (the label takes the hint row and sits at its bottom).

Classes passed through class are merged last: a class that changes display or the columns replaces the layout — use layout="none" for a custom arrangement instead.

How Field reaches the control ​

Field provides a context (id, messages and the most severe state, required, disabled, the message-list id, focusControl, touch). Every Aurora control reads it through useFieldControl and puts id, aria-invalid (errors only), aria-describedby, aria-required and the blur handler on its focusable element, and lights the matching state (missing, error or warning). Nothing is cloned, so the control can sit at any depth — a page-level CountrySelector wrapping SimpleSelect needs no passthrough props.

Precedence: a control's own state props add to the field's (an error prop inside a clean field lights the error; :error="false" cannot clear the field's); disabled and readonly outrank every validation state, so a disabled or read-only control keeps its own look and only the message shows. A control that sets its own id keeps it — and detaches from the label.

FieldLabel is the <label for=id>; FieldHint focuses the control on click as well. When the id landed on a wrapper element (a combobox, a radio group) the first focusable descendant is focused.

Making a control Field-aware ​

ts
const props = defineProps<FormStateProps & { /* yours */ }>()   // error / warning / missing / success / disabled / readonly
const { rootClass, validationMarker, disabled, controlAttrs } = useFieldControl(props)
ReturnBind it where
rootClassthe bordered box (border colour per state, focus, disabled background)
validationMarker<FormFieldValidationIcon :state="validationMarker" /> inside the box
controlAttrsv-bind on the focusable element (input, trigger button, switch), before $attrs so a consumer's explicit id wins. Carries id, aria-* and onBlur
disabledinstead of props.disabled — it is the control's own prop OR the field's

Rules: one root element per control (a fragment puts both roots in the control area); don't put data-field-part on a control; useFieldControl outside a Field is harmless — only the control's own props apply. Something that is not a control (a label with a tooltip, a counter) reads useFieldContext() directly. Radios are a group: put the id on the group wrapper, not on each input.

Binding to a vee-validate form ​

With name, Field binds itself to the surrounding useForm: the schema error becomes the message once the control was blurred (or the form submitted), the schema decides required, and the default slot hands the control its value binding:

html
<Field v-slot="{ componentField }" name="email">
  <FieldLabel>Email</FieldLabel>
  <TextInput v-bind="componentField" />
  <FieldMessages />
</Field>

Explicit messages / required add to the form's: both lists show, deduplicated by text. A handed-in message describes the value the form was reset to (the saved record): while the field is dirty it steps aside, and it returns when the value returns. A rejected submission goes through the form's setErrors, not messages. FieldMessages animates a change only while the form is dirty, so messages that arrive with the record settle in place without motion; outside a form every change animates. Without a form above it, name does nothing and the messages show as given. Blur is reported by the control through the context, so selects, comboboxes and date pickers validate on blur like text inputs. See Forms for the useForm setup.

Messages and state ​

messages is a list of { state, text }. Every message renders under the input, coloured by its state (FormMessage) by FieldMessages; children given to FieldMessages replace that default rendering. The control's own state (border + marker icon) follows the most severe one by this precedence: missing > error > warning > success. An empty list renders nothing. Aurora only knows state and text — where a message came from (rule engine, server, stored validation) is the page's concern, mapped before it reaches the field.

StateMeaning
missingrequired data not provided
errorvalue rejected; blocks save
warningvalue accepted, worth a look
successvalue confirmed by something the user cannot see (matched a registry, verified, saved) — use it sparingly; a plain valid field shows no state

Field — API ​

PropTypeDefaultDescription
fieldIdstringgeneratedDOM id the control gets; FieldLabel's for points at it
namestring—Binds to the vee-validate form above (messages, required, componentField slot prop). Also rendered as data-field-name on the root, so a page can find which fields a section holds
requiredbooleanfalseFieldLabel renders the red asterisk; aria-required on the control
disabledbooleanfalseGreys the parts, cursor-not-allowed, disables the control
messagesFormFieldMessage[][]{ state, text } list; drives the control state and what FieldMessages renders. With name, the list shows only while the field is not dirty
layout'stacked' | 'side' | 'none'from FormSection, else 'stacked'See Layout
classstring—Merged onto the root with twMerge
SlotPropsDescription
defaultcomponentField, dirty (only with name)The parts and exactly one control. dirty: the value differs from the one the form was reset to

Parts ​

PartRendersProps
FieldLabel<label for=id> with the required asterisk; default slot is the textclass
FieldHintdescription text, clicking it focuses the controlclass
FieldControlthe control area: a flex row holding the control and what sits beside it (an action button, a unit)class
FieldMessagesthe message list (aria-describedby target); empty = one row per Field message, children replace themclass

Incorrect / Correct ​

  • Don't compute validation inside the page. Field only renders; the list comes from a validation layer.
  • Don't put two controls in one Field. Both would answer to the one id and state. FieldControl holds one control plus things that are not controls (a button that opens history, a unit label) — see "Two controls that belong together" below.
  • Don't forget <FieldMessages />. Without it the field's messages render nowhere (the dev build warns).
  • Don't set id on a control inside a Field. The control's own id replaces the field's, the label's for then points at nothing and clicking it does nothing. Set the id on the field with fieldId.
  • Pass a stable fieldId when something needs to address the field (deep links, tests) — the db column name works; otherwise let it generate one.

Two controls that belong together ​

An amount with a unit, a range with from/to, a phone with a country code. The field context carries one id, one state and one message list, so two controls inside one Field both answer to the label and both light up. Two ways out:

  • One composite control — when the pair is a single value to the user (amount + unit, range). Build a component that renders both inputs, calls useFieldControl once and binds controlAttrs on the primary input; the secondary one gets its own aria-label. The field sees one control, one message list, one state.
  • Two fields in a row — when each part needs its own label and messages (from / to dates). Put two Fields in a flex row (or a two-column FormSection); each owns its label, state and messages, and vee-validate gets two names.