Form
A complete form composition with fields, submit, validation, and imperative ref control
Installation
Usage
Basic Usage
Compose a form from Form + FormItem + Form.Submit. Group controls without a wrapper (checkbox / radio groups) submit natively — give them a name and they join the payload. The form instance is also available through a ref: setValues fills fields from outside (e.g. resuming a draft fetched from remote), reset clears them, submit triggers a programmatic submission.
"use client";
import { useRef } from "react";
import { Button, Checkbox, Form, FormItem, type FormRef, type FormValues, Radio } from "@/ui";
export default function Demo() {
const formRef = useRef<FormRef>(null);
const handleSubmit = async (values: FormValues) => {
console.log("submitted:", values);
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
};
return (
<div className="w-full max-w-md">
<div className="mb-4 flex gap-2">
<Button
variant="outline"
onClick={() => {
formRef.current?.setValues({
name: "Jane Doe",
email: "[email protected]",
interests: ["reading", "music"],
contact: "email",
});
}}
>
Fill sample
</Button>
<Button
variant="outline"
onClick={() => {
formRef.current?.reset();
}}
>
Reset
</Button>
</div>
<Form ref={formRef} onSubmit={handleSubmit} className="space-y-4">
<FormItem name="name" label="Name" controlProps={{ placeholder: "Enter your name" }} />
<FormItem
name="email"
label="Email"
controlProps={{ type: "email", placeholder: "Enter your email" }}
/>
<div className="space-y-1">
<span className="block text-sm font-medium indent-2 select-none">Interests</span>
<Checkbox.Group
name="interests"
options={[
{ label: "Reading", value: "reading" },
{ label: "Music", value: "music" },
{ label: "Sports", value: "sports" },
]}
/>
</div>
<div className="space-y-1">
<span className="block text-sm font-medium indent-2 select-none">Contact preference</span>
<Radio.Group
name="contact"
className="flex-row"
options={[
{ label: "Email", value: "email" },
{ label: "Phone", value: "phone" },
{ label: "SMS", value: "sms" },
]}
/>
</div>
<Form.Submit>Submit</Form.Submit>
</Form>
</div>
);
}
Validation
Wrapped fields validate through FormItem's validate / validateTrigger. Validation runs on onChange or onBlur and the returned string renders as the error message below the input.
"use client";
import { Form, FormItem } from "@/ui";
export default function FormValidationDemo() {
return (
<Form
className="w-sm space-y-4"
onSubmit={async () => {
await new Promise((r) => setTimeout(r, 600));
return true;
}}
>
<FormItem
name="username"
label="Username"
description="At least 3 characters"
controlProps={{ placeholder: "Pick a username" }}
validate={(value) => {
if (value.length === 0) return "Username is required";
if (value.length < 3) return "Must be at least 3 characters";
return null;
}}
validateTrigger="onChange"
/>
<FormItem
name="email"
label="Email"
controlProps={{ type: "email", placeholder: "[email protected]" }}
validate={(value) => {
if (value.length === 0) return "Email is required";
if (!/.+@.+\..+/.test(value)) return "Enter a valid email";
return null;
}}
validateTrigger="onBlur"
/>
<Form.Submit>Sign up</Form.Submit>
</Form>
);
}
Parts Mode (useFieldValidity)
When FormItem is too much, skip wrappers entirely: keep a useFieldValidity store (built into the Form module), set/clear errors from your own handlers, pass aria-invalid into the control, and render your own description / error text. Every input control ships danger styling (border-danger / text-danger and a danger focus ring) that activates from the aria-invalid attribute alone.
"use client";import { useState } from "react";import { Input, useFieldValidity } from "@/ui";export default function Demo() { const { validity, setFieldError } = useFieldValidity(); const [email, setEmail] = useState(""); return ( <div className="flex w-full max-w-md flex-col gap-1"> <Input value={email} placeholder="[email protected]" aria-invalid={Boolean(validity.email) || undefined} onChange={(e) => { const next = e.target.value; setEmail(next); setFieldError( "email", /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(next) ? null : "Enter a valid email", ); }} /> {validity.email ? ( <small role="alert" className="text-danger indent-2 text-sm"> {validity.email} </small> ) : ( <small className="text-muted-foreground indent-2 text-sm"> Parts mode — no wrapper, styles come from aria-invalid </small> )} </div> );}API Reference
High-level Components
Ready-to-use composite components.
Form
The root form component built on top of the native <form> element.
| Prop | Type | Default | Description |
|---|---|---|---|
onSubmit | (values: FormValues) => Promise<boolean> | - | Async submit handler. Receives a preprocessed object where multi-value fields are arrays. Return true to mark success and trigger autoReset |
autoReset | boolean | true | Reset the form when onSubmit resolves to true |
onReset | () => void | - | Called whenever the form resets — after a successful submit with autoReset, or via ref.reset() |
ref | React.Ref<FormRef> | - | Imperative handle exposing setValues, reset, submit |
children | React.ReactNode | - | Form contents, typically FormItem and Form.Submit |
...props | React.ComponentProps<"form"> | - | Supports all native form props (except onSubmit, ref), spread onto the underlying form element |
FormRef
Imperative handle returned by the ref prop, where FormValue = string | number | string[] | number[] | null.
| Method | Type | Description |
|---|---|---|
setValues | (values: Record<string, FormValue>) => void | Update one or more fields and dispatch a change event on each modified input |
reset | () => void | Reset the form and clear all registered inputs |
submit | () => void | Trigger a programmatic form submission |
Form.Submit
A submit button that automatically reflects the pending state of the form. It is disabled while the form is submitting and can render a custom loading icon.
| Prop | Type | Default | Description |
|---|---|---|---|
loadingIcon | React.ReactNode | - | Custom icon rendered while the form is pending. Defaults to a spinning Loader2 |
className | string | - | Custom CSS class, merged with the built-in button styles |
children | React.ReactNode | - | Button label |
All other native <button> props are supported; type is forced to submit.
Context Menu
An imperative right-click context menu with pointer anchoring, grouping and submenus — built on Menu and native popover, opened with a single call
Form Item
A self-managing form field that renders a finished control per variant — label, input and a description / error line in a fixed vertical layout with built-in validation