Form
A complete form composition with fields, submit, validation, and imperative ref control
Installation
- $npx litefy@latest add form
- $pnpm dlx litefy@latest add form
- $yarn dlx litefy@latest add form
- $bun --bun litefy@latest add form
Usage
Compose a form from Form + FormItem + Form.Submit; the form instance is also available through a ref (setValues / reset / submit).
"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: "jane@example.com",
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.
"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: "you@example.com" }}
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)
Roll your own field wiring with the useFieldValidity hook instead of FormItem wrappers.
"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="you@example.com" 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.
Field Search
An all-in-one search control pairing a leading field select with a value input — text fields filter on debounce, enum fields search on pick.
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