A form that holds the values, runs your validators or a schema, and exposes its progress as an action state (idle, pending, success, error). Fields connect by name and the inputs already link label, help text and error. It never fetches or saves anything: your onSubmit does.
import { Form, FormCheckbox, FormSubmitButton, FormTextField } from "@rdloom/react";
export default function FormBasicExample() {
return (
<Form
className="w-80"
defaultValues={{ email: "", password: "", remember: false }}
onSubmit={(values) => {
// Send the values to your own sign-in call here.
console.log(values);
}}
>
<FormTextField name="email" label="Email" type="email" autoComplete="email" isRequired />
<FormTextField name="password" label="Password" type="password" autoComplete="current-password" isRequired />
<FormCheckbox name="remember">Keep me signed in</FormCheckbox>
<FormSubmitButton>Sign in</FormSubmitButton>
</Form>
);
}Installation
npx rdloom add formCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs @tanstack/react-form, react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/form.jsonWorks, but without upgrade tracking.
Usage
import { Form, FormCheckbox, FormSubmitButton, FormTextField } from "@rdloom/react";
<Form
className="w-80"
defaultValues={{ email: "", password: "", remember: false }}
onSubmit={(values) => {
// Send the values to your own sign-in call here.
console.log(values);
}}
>
<FormTextField name="email" label="Email" type="email" autoComplete="email" isRequired />
<FormTextField name="password" label="Password" type="password" autoComplete="current-password" isRequired />
<FormCheckbox name="remember">Keep me signed in</FormCheckbox>
<FormSubmitButton>Sign in</FormSubmitButton>
</Form>Validation
import { ErrorSummary, Form, FormSelect, FormSubmitButton, FormTextField, SelectItem } from "@rdloom/react";
const looksLikeEmail = (value: string) => (/^\S+@\S+\.\S+$/.test(value) ? null : "Enter an email address like name@example.com.");
export default function FormValidationExample() {
return (
<Form className="w-96" defaultValues={{ name: "", email: "", role: null }} onSubmit={() => {}}>
{/* After a failed submit focus moves here; each item takes you to its field. */}
<ErrorSummary />
<FormTextField name="name" label="Full name" isRequired requiredMessage="Enter the person's full name." />
<FormTextField
name="email"
label="Email"
type="email"
description="We send the invitation here."
isRequired
validate={looksLikeEmail}
/>
<FormSelect name="role" label="Role" placeholder="Choose a role" isRequired>
<SelectItem id="admin">Admin</SelectItem>
<SelectItem id="editor">Editor</SelectItem>
<SelectItem id="viewer">Viewer</SelectItem>
</FormSelect>
<FormSubmitButton>Send invitation</FormSubmitButton>
</Form>
);
}Async submit
import { Alert, ErrorSummary, Form, FormSubmitButton, FormTextField } from "@rdloom/react";
// Stands in for your own request; it fails for one address so the error state shows.
async function saveProfile(values: { name: string; email: string }) {
await new Promise((resolve) => setTimeout(resolve, 800));
if (values.email === "taken@example.com") {
return { fieldErrors: { email: "That email is already in use." } };
}
}
export default function FormAsyncSubmitExample() {
return (
<Form
className="w-96"
defaultValues={{ name: "Ada Lovelace", email: "ada@example.com" }}
onSubmit={saveProfile}
successMessage="Profile saved."
>
{({ state }) => (
<>
<ErrorSummary />
{state === "success" && <Alert variant="success">Profile saved.</Alert>}
<FormTextField name="name" label="Full name" isRequired />
<FormTextField name="email" label="Email" type="email" isRequired description="Try taken@example.com to see the error state." />
<FormSubmitButton>{state === "pending" ? "Saving" : "Save profile"}</FormSubmitButton>
</>
)}
</Form>
);
}Conditional field
import { Form, FormSubmitButton, FormSwitch, FormTextField, useFormValues } from "@rdloom/react";
type Values = { name: string; invoiceByPost: boolean; address: string };
// A field that exists only while a choice is on: it is checked only while it is shown.
function Address() {
const byPost = useFormValues<Values, boolean>((v) => v.invoiceByPost);
return byPost ? <FormTextField name="address" label="Postal address" multiline isRequired /> : null;
}
export default function FormConditionalFieldExample() {
return (
<Form<Values> className="w-96" defaultValues={{ name: "", invoiceByPost: false, address: "" }} onSubmit={() => {}}>
<FormTextField name="name" label="Company name" isRequired />
<FormSwitch name="invoiceByPost">Send invoices by post</FormSwitch>
<Address />
<FormSubmitButton>Save</FormSubmitButton>
</Form>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
defaultValuesrequiredThe starting values, shaped like the data you submit. Nested objects and arrays work. | Record<string, any> | none |
onSubmitrequiredCalled with the values when every check passes. Await your own request here. Throw, or return { fieldErrors, formError }, to report a failure; the form goes to the error state. | (values: any) => void | SubmitResult | Promise<void | SubmitResult> | none |
schemaA schema from any library that follows the Standard Schema spec (Zod, Valibot, ArkType). Its issues land on the fields by path. | FormSchema | none |
validateOnWhen errors first appear. After a failed submit they always update as the person types. | "submit" | "blur" | "change" | "submit" |
successMessageRead out politely by screen readers after a successful submit. Show your own visible confirmation as well. | string | none |
errorMessageThe form-level message when onSubmit throws. | string | "We could not complete that. Try again." |
resetOnSuccessPut the default values back after a successful submit. | boolean | false |
childrenThe fields and buttons, or a function that receives { state, isPending, formError, error }. Inside, useFormState() and useFormValues() read the same. | ReactNode | ((form: FormRenderState) => ReactNode) | none |
Accessibility
Role form, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab moves through the fields in order
- Enter in a text field submits the form
- A failed submit moves focus to the ErrorSummary; each item moves focus to its field
Screen readers announce
- Submitting with problems: the summary heading is read, then the list of problems; each is a link to its field
- Moving to a field with an error: label, then the error text, then that it is invalid
- After a successful submit: the success message is read without moving focus
- While the request runs, the submit button is read as busy
What your code must do
- The browser checks are off (noValidate); the validators and schema you pass produce every message, so they read the same everywhere
- Each field links its label, help text and error with aria-describedby and sets aria-invalid
- Errors do not appear before the person has finished: on submit by default, then live while fixing
- A repeat submit is ignored while one is pending, and the submit button announces the pending state
- Success and failure are announced in a polite status region (a failure is left to the ErrorSummary when one is on the page)
Guidelines
Use it when
- Any form with more than one field, validation, or an async submit
- Forms with repeating rows (use FieldArray inside)
Avoid it when
- A single search box or filter that applies as you type: use a plain TextField
- Fetching, saving or caching: do that in your onSubmit with your own data layer
Don't
- Putting totals or other derived numbers in the library: read values with useFormValues and calculate in your own code
- Passing isInvalid to an input inside a Field: the Field sets it from the errors
- Validating on every keystroke before the person has submitted or left the field: leave validateOn on submit or blur
- Using color alone for an error: the message text is always shown and read
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-text-default--rd-color-text-muted--rd-color-border-default--rd-color-surface-default--rd-color-feedback-danger--rd-color-focus-ring