Preview
<Form onSubmit={(e) => { e.preventDefault(); }}>
<Grid columns={2} gap="md">
<Field name="firstName">
<Field.Label>First name</Field.Label>
<Field.Control>
<Input placeholder="Jane" />
Usage
Use when
- The server rejects a submission and each reason must reach its own field
- Client and server validation live in the same form
Avoid when
- Client-side validation only (a native form plus Field is enough)
- Layout without submission (use Grid or Card)
Implementation notes
- errors is keyed by field name: Record<string, string | string[]>
- A field's server error clears as soon as the person edits that field
- A Field picks up its error by matching name
- Leave Field.Error unmatched so it shows whatever the server sent
- Put a Grid inside Form or Fieldset for multi-column layouts
- Buttons go in Form.Actions so they stay right-aligned
- Set pending while the submit is in flight, and give the submit Button pending too
Installation
pnpm add @usefragments/ui
Import styles once from @usefragments/ui/styles. See the installation guide for framework and theme setup.
Anatomy
Import Form once, then compose its parts with dot notation.
Paired name fields, then full-width rows and a right-aligned submit.
<Form onSubmit={(e) => { e.preventDefault(); }}>
<Grid columns={2} gap="md">
<Field name="firstName">
<Field.Label>First name</Field.Label>
<Field.Control>
<Input placeholder="Jane" />
</Field.Control>
</Field>
<Field name="lastName">
<Field.Label>Last name</Field.Label>
<Field.Control>
<Input placeholder="Doe" />
</Field.Control>
</Field>
<Grid.Item colSpan="full">
<Field name="email">
<Field.Label>Email</Field.Label>
<Field.Control>
Examples
Profile settings
Fieldsets split a long form into sections the user can scan.
<Form onSubmit={(e) => { e.preventDefault(); }}>
<Fieldset>
<Fieldset.Legend>Profile</Fieldset.Legend>
<Grid columns={2} gap="md">
<Field name="displayName">
<Field.Label>Display Name</Field.Label>
Contact form
Mixed control types in one submission flow.
<Form onSubmit={(e) => { e.preventDefault(); }}>
<Grid columns={2} gap="md">
<Field name="name">
<Field.Label>Name</Field.Label>
<Field.Control>
<Input placeholder="Your name" />
With server errors
Errors from the server land on the fields that caused them.
<Form errors={{ username: 'Username is already taken', email: 'Email is already registered' }}>
<Grid columns={2} gap="md">
<Field name="username">
<Field.Label>Username</Field.Label>
<Field.Control>
<Input defaultValue="janedoe" />
Pending
Fields lock and the submit shows its working state while the request runs.
<Form pending onSubmit={(e) => { e.preventDefault(); }}>
<Field name="email">
<Field.Label>Email</Field.Label>
<Input type="email" defaultValue="jane@example.com" />
</Field>
<Form.Actions>
API reference
Component props | | | |
|---|
Actions.childrenRequired | React.ReactNode | Not set | — |
childrenRequired | React.ReactNode | Not set | Form content |
errors | Record<string, string | string[]> | undefined | Not set | Server-side errors keyed by field name |
onSubmit | ((event: React.FormEvent<HTMLFormElement>) => void) | undefined | Not set | Form submission handler |
pending | boolean | undefined | false | A submit is in flight: the form is aria-busy and every control inside is locked until it settles |
validationMode | "onBlur" | "onChange" | "onSubmit" | undefinedonBluronChangeonSubmit | Not set | When field validation should run |
Accessibility
- Renders a real form element
- Errors link back to their field via aria-describedby
- pending sets aria-busy and disables every control inside
Related components
- FieldsiblingContains Field components for error distribution
- FieldsetsiblingUse Fieldset to group fields within a Form
- ButtonsiblingUse Button type="submit" for form submission
Next steps
- Accessibility guidanceThe keyboard, focus and naming rules every component carries.
- Every componentThe catalog, grouped by what each component does.