Form & Field
StableForm provides layout and react-hook-form context. Field is the missing layer every project rebuilds: label + input + hint + error, automatically wired together with correct ARIA associations.
Form elementContext providerreact-hook-form
Interactive example
Contact form with Field
Import
tsx
import { Form, Field } from '@vhyxui/react'Field
Field is the differentiator. Developers rebuild this on every project. VhyxUI makes it first class.
Please enter a valid email address.
Field with error
At least 8 characters, including a number.
Field with hint
Required and optional indicators
Layouts
Vertical (default)
Disabled form
Props
Form
| Prop | Type | Default | Description |
|---|---|---|---|
form | UseFormReturn<FieldValues> | — | react-hook-form return value. When provided, errors propagate automatically from form state. |
layout | 'vertical' | 'horizontal' | 'inline' | 'vertical' | Field layout direction within this form. |
size | 'sm' | 'md' | 'lg' | 'md' | Size propagated to all form controls. |
disabled | boolean | false | Disables all form controls at once. |
onSubmit | (data: FieldValues) => void | Promise<void> | — | Called with validated form data on submission. |
Also accepts all standard HTMLFormElement attributes.
Field
| Prop | Type | Default | Description |
|---|---|---|---|
name * | string | — | Field name — used to read errors from react-hook-form context. |
label | React.ReactNode | — | Visible label rendered above the input. |
hint | React.ReactNode | — | Helper text rendered below the input. |
error | string | — | Error message. Overrides any error from react-hook-form context. |
required | boolean | — | Shows required indicator (*) and sets aria-required on the child. |
optional | boolean | — | Shows an optional indicator next to the label. |
layout | 'vertical' | 'horizontal' | — | Overrides the Form layout for this specific field only. |
children | React.ReactNode | — | The input component to render. |
Accessibility
- Field generates unique IDs and connects
label[for]→ input automatically. - Hint and error text are connected via
aria-describedby— announced by screen readers. - Error text has
role="alert"— announced immediately when it appears. -
aria-invalidpropagated to child input when an error is present. - Required indicator (*) is decorative — screen readers hear "required" from
aria-required. - Disabled Form sets
disabledon all child inputs andaria-disabledwhere needed.
Keyboard navigation
| Key | Action |
|---|---|
| Tab | Move focus to the next field or focusable element. |
| Shift + Tab | Move focus to the previous field or focusable element. |
| Enter | Submit the form when focus is on a submit button. |
Agent contract
Default VhyxSeal contract shipped with every Form.
Default contract
{
"type": "action",
"intent": "submit-form",
"description": "Collects field values and submits them to the server or handler",
"requires": [],
"requiredPermissions": [],
"consequence": "Sends form data to the configured handler — may create or update records",
"affects": [
"form"
],
"reversible": false,
"safetyLevel": "medium",
"requiresConfirmation": false,
"destructive": false,
"contractVersion": "0.0.1",
"fingerprint": "vhyxs_082c611e"
}Individual field inputs carry their own contracts. The Form contract represents the submission action.
Theming
Override these CSS tokens to theme Form and Field.
--vhyx-text-smLabel font size
--vhyx-weight-mediumLabel font weight
--vhyx-color-dangerError text color
--vhyx-color-danger-textError message color
--vhyx-color-text-mutedHint text color
--vhyx-color-text-subtleOptional indicator color
--vhyx-space-1-5Label bottom gap
--vhyx-space-4Field gap (between label and input)
Examples
react-hook-form + zod
tsx
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, Field, Input, Button } from '@vhyxui/react'
const schema = z.object({
email: z.string().email('Please enter a valid email'),
password: z.string().min(8, 'At least 8 characters required'),
})
function LoginForm() {
const form = useForm({ resolver: zodResolver(schema) })
return (
<Form form={form} onSubmit={form.handleSubmit(onSubmit)}>
<Field name="email" label="Email" required>
<Input
{...form.register('email')}
type="email"
placeholder="you@example.com"
error={!!form.formState.errors.email}
/>
</Field>
<Field name="password" label="Password" hint="At least 8 characters" required>
<Input
{...form.register('password')}
type="password"
error={!!form.formState.errors.password}
/>
</Field>
<Button type="submit" loading={form.formState.isSubmitting}>
Log in
</Button>
</Form>
)
}Manual error control
tsx
<Form>
<Field name="username" label="Username" error={serverError}>
<Input value={username} onChange={e => setUsername(e.target.value)} />
</Field>
</Form>