icon

Morainev0.5.0

FormField
formsform-field

FormField

Form field wrapper providing label, description, and validation message layout.

Import#

import { createForm, Form, FormField } from 'moraine'

Inside Form, a named field automatically reads its value and validation state from Formisch. Top-level fields accept a string name; nested and array fields use a path such as ['users', 0, 'email']. Outside Form, the component remains a standalone layout primitive.

Pass the schema type to validate names at compile time:

<FormField<typeof schema> name={['profile', 'email']} label="Email">
<Input />
</FormField>

Slot Structure#

Label, description, and hint area above a control slot with validation feedback.

root
├── wrapper
│ ├── labelWrapper (optional)
│ │ ├── label
│ │ └── hint (optional)
│ └── description (optional)
└── {children}
├── help (optional)
└── error (optional)

Examples#

Basic#

Label, hint, description, and help text with a single control.

function Basic() {
return (
<div class="mx-auto max-w-xl w-full">
<FormField
label="Workspace Name"
hint="Required"
description="Name used in URLs and workspace-level permissions."
help="Use lowercase letters, numbers, and dashes."
required
>
<Input placeholder="acme-platform" />
</FormField>
</div>
)
}

With Validation#

Bind to a Formisch store and show validation feedback without a render prop.

function WithValidation() {
const form = createForm({
schema: v.object({ email: v.pipe(v.string(), v.email('Enter a valid email.')) }),
initialInput: { email: '' },
})
return (
<Form of={form} class="mx-auto max-w-xl w-full space-y-4">
<FormField name="email" label="Owner Email" required>
<Input type="email" placeholder="[email protected]" />
</FormField>
<Button type="submit">Save</Button>
</Form>
)
}

Horizontal Layout#

Use orientation="horizontal" for form-like row layouts.

function HorizontalLayout() {
return (
<div class="mx-auto max-w-2xl w-full space-y-4">
<FormField
orientation="horizontal"
label="Display Name"
description="Public name shown in activity feeds."
>
<Input placeholder="Moraine Team" />
</FormField>
<FormField orientation="horizontal" label="Default Role" required>
<Select
options={[
{ label: 'Developer', value: 'developer' },
{ label: 'Designer', value: 'designer' },
{ label: 'Manager', value: 'manager' },
]}
placeholder="Select role"
/>
</FormField>
</div>
)
}

Sizes#

Preview the field typography scale from xs to xl.

function Sizes() {
const SIZES: FormFieldSizeName[] = ['xs', 'sm', 'md', 'lg', 'xl']
type FormFieldSizeName = Exclude<FormFieldT.Variant['size'], undefined>
return (
<div class="gap-4 grid sm:grid-cols-2">
<For each={SIZES}>
{(size) => (
<FormField
size={size}
label={`Workspace Name (${size})`}
description="Name used in URLs and workspace-level permissions."
help="Use lowercase letters, numbers, and dashes."
>
<Input size={size} placeholder={`acme-platform-${size}`} />
</FormField>
)}
</For>
</div>
)
}

Manual Error#

Force error state with a custom error message.

function ManualError() {
return (
<div class="mx-auto max-w-xl w-full">
<FormField
label="Access Token"
hint="Required"
error="Token has expired. Generate a new token and retry."
>
<Input type="password" value="********" />
</FormField>
</div>
)
}

Render Context#

Use render-function children to react to error state.

function RenderContext() {
const form = createForm({
schema: v.object({
releaseTitle: v.pipe(v.string(), v.nonEmpty('Release title is required.')),
}),
initialInput: { releaseTitle: '' },
})
return (
<Form of={form} class="mx-auto max-w-xl w-full space-y-4">
<FormField name="releaseTitle" label="Release Title" required>
{(props) => <Input placeholder={props.error ? 'Title is required' : 'v2.14.0'} />}
</FormField>
<Button type="submit">Create Draft</Button>
</Form>
)
}

Nested Path#

Use array path names for nested form fields.

function NestedPath() {
const form = createForm({
schema: v.object({
profile: v.object({
name: v.pipe(v.string(), v.nonEmpty('Name is required.')),
email: v.pipe(v.string(), v.email('Valid email is required.')),
}),
}),
initialInput: { profile: { name: '', email: '' } },
})
return (
<Form of={form} class="mx-auto max-w-xl w-full space-y-4">
<FormField name={['profile', 'name']} label="Profile Name" required>
<Input placeholder="Moraine Team" />
</FormField>
<FormField name={['profile', 'email']} label="Profile Email" required>
<Input type="email" placeholder="[email protected]" />
</FormField>
<Button type="submit">Save Profile</Button>
</Form>
)
}

API Reference#

Attributes#

Slotroot1 attribute
Field wrapper that links label, control, description, and messages.

Data Attributes

1
Data AttributeDescription
data-orientation
Stores the rendered orientation.

Props#

PropTypeDefaultDescription
asT | undefineddiv
The HTML element or component to render as.
childrenComponentOrElement<FormFieldT.RenderContext> | undefined
Children of the field, can be a render function.
classClassValue
Class applied to the component root or trigger element.
classesFormFieldT.Classes | undefined
descriptionJSX.Element
Description text shown below the label.
errorboolean | string | JSX.Element
Custom error message or force error state.
helpJSX.Element
Help text shown below the control when no error is present.
hintJSX.Element
Hint text shown near the label.
idstring | undefined
Unique identifier for the form field.
labelJSX.Element
Label for the field.
nameFormFieldT.Name<TSchema> | undefined
The name of the field (key in form state).
orientation"horizontal" | "vertical" | undefined
refJSX.HTMLElementTags[T] extends { ref?: infer Ref; } ? Ref : never | undefined
requiredboolean | undefinedfalse
Whether the field is required.
size"xs" | "sm" | "md" | "lg" | "xl" | undefined
styleJSX.CSSProperties | undefined
stylesFormFieldT.Styles | undefined