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
Installation
Usage
Basic Usage
FormItem renders a finished control per variant (input, textarea, select, password, number-input) with a fixed vertical layout: label → input → description / error (the two lines share one slot — the description shows by default and is replaced by the error while the field is invalid). It registers with the surrounding Form so its value is collected on submit, shows validation errors in a danger line below the input, and works standalone too.
"use client";
import { useRef } from "react";
import { Button, Form, FormItem, type FormRef } from "@/ui";
export default function Demo() {
const formRef = useRef<FormRef>(null);
return (
<Form
ref={formRef}
className="w-sm space-y-4"
onSubmit={async (values) => {
console.log(values);
return true;
}}
>
<FormItem
variant="input"
name="username"
label="Username"
required
description="3-16 characters"
validate={(value) => {
if (!value) return "Username is required";
if (value.length < 3 || value.length > 16) return "Must be 3-16 characters";
return null;
}}
/>
<FormItem
variant="password"
name="password"
label="Password"
required
validate={(value) => (value.length >= 8 ? null : "At least 8 characters")}
/>
<FormItem
variant="select"
name="role"
label="Role"
validate={(value) => (value ? null : "Pick a role")}
controlProps={{
options: [
{ label: "Developer", value: "developer" },
{ label: "Designer", value: "designer" },
],
}}
/>
<FormItem
variant="number-input"
name="age"
label="Age"
validateTrigger="onChange"
validate={(value) => {
if (!value) return null;
const n = Number(value);
return Number.isFinite(n) && n >= 1 && n <= 120 ? null : "1-120";
}}
/>
<div className="flex gap-2 pt-2">
<Button type="submit">Submit</Button>
<Button type="button" variant="outline" onClick={() => formRef.current?.reset()}>
Reset
</Button>
</div>
</Form>
);
}
Validation
validate supports two error shapes: return a string for a hard error (message plus the danger border / aria-invalid on the control), or { message, invalid: false } for a message-only error — handy for empty required fields, where the hint text should appear but the control keeps its normal style until a wrong value is actually entered.
"use client";
import { Form, FormItem } from "@/ui";
export default function Demo() {
return (
<Form className="w-md" onSubmit={async () => true}>
<FormItem
variant="input"
name="repo"
label="Repository"
description="Required. Letters, numbers and dashes once filled."
validate={(value) => {
if (value === "") return { message: "Repository is required", invalid: false };
if (!/^[a-z0-9-]+$/i.test(value)) return "Only letters, numbers and dashes";
return null;
}}
controlProps={{ placeholder: "litefy-ui" }}
/>
</Form>
);
}
API Reference
FormItem
A form field rendered as a finished control. The prop set is a discriminated union on variant: every variant shares FormItemBaseProps, and controlProps is typed by the selected variant.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "input" | "textarea" | "select" | "password" | "number-input" | "input" | Which finished control to render |
name | string | - | Field name — also the key the surrounding Form uses to collect values. Required |
label | React.ReactNode | - | Label rendered above the control |
description | React.ReactNode | - | Helper text rendered below the control; replaced by the error message while the field is invalid |
required | boolean | - | Marks the label and makes the control required |
validate | (value: string) => string | { message: string; invalid?: boolean } | boolean | null | undefined | Promise<...> | - | A string is a hard error (message + aria-invalid on the control); { message, invalid: false } shows the message without styling the control; false is a bare invalid state; null/undefined/true pass |
validateTrigger | "onChange" | "onBlur" | "onBlur" | When validate runs. Once the field is invalid, it also re-validates on every change |
disabled | boolean | - | Disables the control |
className | ClassNameValue | - | Root container custom styles |
classNames | { label?; description?; error? } — each ClassNameValue | - | Custom classes for the label, description and error lines |
controlProps | Per variant, see below | {} | Props forwarded to the underlying control |
controlProps by variant:
| Variant | controlProps type |
|---|---|
input | Omit<InputProps, "ref"> |
textarea | Omit<TextareaProps, "ref"> |
select | Omit<SelectProps, "ref"> & { options: (SelectOption | SelectOptionGroup)[] } |
password | Omit<PasswordProps, "ref"> |
number-input | Omit<NumberInputProps, "ref"> |
Custom
onChange/onBlurincontrolPropsare preserved —FormItemwraps them, so validation still runs alongside your handlers. For theselectvariant the value lives in component state and a hidden input carries it into native form submission.