Form
Form composes native validation, labels, controls and messages without duplicating field ownership.
- Updated
Design notes
Combine FormLabel and FormControl asChild with Input or TextareaInput. Non-native controls such as Select need their own validation and focus handling.
Playground
Contact preferences
Support request
Local example. Nothing is sent to support.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225import { useRef, useState } from "react"
import {
Button,
Checkbox,
Radio,
Select,
Switch,
Input,
TextareaInput,
Form,
FormField,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormFieldset,
FormLegend,
FormActions,
FormSubmit,
} from "@yuzur1ha/ui"
import { DemoResult } from "../../examples/result"
export function FormDemo() {
const [serverInvalid, setServerInvalid] = useState(false)
const [result, setResult] = useState("No changes submitted.")
return (
<>
<section className="form-demo-group" aria-label="Contact form example">
<h2>Contact preferences</h2>
<Form
onClearServerErrors={() => setServerInvalid(false)}
onReset={() => setResult("No changes submitted.")}
onSubmit={(event) => {
event.preventDefault()
const data = new FormData(event.currentTarget)
const email = data.get("email")
if (email === "taken@example.test") {
setServerInvalid(true)
return
}
const delivery =
data.get("delivery") === "daily" ? "daily" : "immediate"
setResult(
`Saved locally: ${typeof email === "string" ? email : ""} · ${delivery} · notifications ${data.has("notifications") ? "on" : "off"} · product updates ${data.has("updates") ? "on" : "off"}`,
)
}}
>
<FormFieldset>
<FormLegend>Contact preferences</FormLegend>
<FormField name="email" serverInvalid={serverInvalid}>
<FormLabel>Email address</FormLabel>
<FormControl asChild>
<Input
type="email"
required
autoComplete="email"
placeholder="you@example.test"
onChange={() => setServerInvalid(false)}
/>
</FormControl>
<FormDescription>
Demo only. Try taken@example.test to see a simulated error.
</FormDescription>
<FormMessage match="valueMissing">
Enter your email address.
</FormMessage>
<FormMessage match="typeMismatch">
Enter a valid email address.
</FormMessage>
{serverInvalid && (
<FormMessage>This email is already in use.</FormMessage>
)}
</FormField>
<FormField name="bio">
<FormLabel>About you</FormLabel>
<FormControl asChild>
<TextareaInput
maxLength={160}
placeholder="A few words about yourself."
/>
</FormControl>
<FormDescription>Optional. Up to 160 characters.</FormDescription>
</FormField>
</FormFieldset>
<FormFieldset>
<FormLegend>Delivery preferences</FormLegend>
<Switch
label="Email notifications"
name="notifications"
defaultChecked
/>
<fieldset className="form-demo-choices">
<legend className="demo-label">Delivery schedule</legend>
<Radio
label="Immediately"
name="delivery"
value="immediate"
defaultChecked
/>
<Radio label="Daily summary" name="delivery" value="daily" />
</fieldset>
<Checkbox label="Include product updates" name="updates" />
</FormFieldset>
<FormFieldset disabled>
<FormLegend>Unavailable settings</FormLegend>
<FormField name="organization">
<FormLabel>Organization</FormLabel>
<FormControl defaultValue="Personal workspace" />
</FormField>
</FormFieldset>
<FormActions>
<FormSubmit>Save preferences</FormSubmit>
<Button appearance="secondary" type="reset">
Reset
</Button>
</FormActions>
</Form>
<DemoResult>{result}</DemoResult>
</section>
<SupportRequestDemo />
</>
)
}
function SupportRequestDemo() {
const topicControl = useRef<HTMLButtonElement>(null)
const [topic, setTopic] = useState("")
const [topicError, setTopicError] = useState("")
const [failed, setFailed] = useState(false)
const [result, setResult] = useState("No sample request submitted.")
return (
<section className="form-demo-group" aria-label="Support form example">
<h2>Support request</h2>
<p className="demo-label">Local example. Nothing is sent to support.</p>
<Form
aria-label="Sample support request"
onReset={() => {
setTopic("")
setTopicError("")
setFailed(false)
setResult("No sample request submitted.")
}}
onSubmit={(event) => {
event.preventDefault()
if (!topic) {
setTopicError("Choose a request topic.")
topicControl.current?.focus()
return
}
const data = new FormData(event.currentTarget)
if (data.has("simulateFailure")) {
setFailed(true)
setResult("No sample request submitted.")
return
}
setFailed(false)
const subject = data.get("subject")
setResult(
`Saved locally: ${typeof subject === "string" ? subject : ""} · ${topic} · follow-up ${data.has("followUp") ? "on" : "off"}`,
)
}}
>
<FormFieldset>
<FormLegend>Request details</FormLegend>
<Select
ref={topicControl}
label="Request topic"
placeholder="Choose a topic"
name="topic"
value={topic}
onValueChange={(value) => {
setTopic(value)
setTopicError("")
}}
{...(topicError ? { error: topicError } : {})}
options={[
{ value: "connection", label: "Connection & setup" },
{ value: "account", label: "Account access" },
{ value: "general", label: "General question" },
]}
/>
<FormField name="subject">
<FormLabel>Subject</FormLabel>
<FormControl asChild>
<Input required placeholder="What do you need help with?" />
</FormControl>
<FormMessage match="valueMissing">Enter a subject.</FormMessage>
</FormField>
<FormField name="details">
<FormLabel>Request details</FormLabel>
<FormControl asChild>
<TextareaInput
required
placeholder="Describe the issue using sample information."
/>
</FormControl>
<FormDescription>
Do not include passwords, codes or configuration credentials.
</FormDescription>
<FormMessage match="valueMissing">Describe the issue.</FormMessage>
</FormField>
<Checkbox
label="Email me about this sample request"
name="followUp"
defaultChecked
/>
</FormFieldset>
<Checkbox label="Simulate submission failure" name="simulateFailure" />
{failed && (
<p className="form-demo-error" role="alert">
Demo submission failed. Your draft is preserved; turn off the
simulation and retry.
</p>
)}
<FormActions>
<FormSubmit>Submit sample request</FormSubmit>
<Button appearance="secondary" type="reset">
Reset request
</Button>
</FormActions>
</Form>
<DemoResult>{result}</DemoResult>
</section>
)
}
API
Form
Radix form root using native constraint validation. Use FormField, FormLabel and FormControl for native controls, not a nested TextField or non-native Select.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
onClearServerErrors | (() => void) | No | — | Clear previous server errors before the submit callback and after an uncancelled reset. Editing alone does not invoke it. |
onSubmit | SubmitEventHandler<HTMLFormElement> | No | — | Called after native validation permits form submission. Prevent default for client-side handling. |
onReset | ReactEventHandler<HTMLFormElement> | No | — | Native reset handler. Prevent default to cancel reset and server-error clearing. |
name | string | No | — | Native form submission field name. |
target | string | No | — | Browsing context for the linked destination, such as _self or _blank. |
action | string | ((formData: FormData) => void | Promise<void>) | No | — | Native form submission destination or React form action. |
method | string | No | — | Native form submission method. |
noValidate | boolean | No | — | Skip built-in constraint validation during form submission. |
children | ReactNode | No | — | Content rendered inside the component. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
asChild | boolean | No | — | Render through one child element instead of the default element. The child must forward its ref and accept the supplied props. |
ref | Ref<HTMLFormElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormField
Groups one named native field with its label and validation messages. Native div attributes are forwarded.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Field name shared by its native control, label and validation messages. |
serverInvalid | boolean | No | — | Mark the field invalid from a server response. |
children | ReactNode | No | — | Content rendered inside the component. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
asChild | boolean | No | — | Render through one child element instead of the default element. The child must forward its ref and accept the supplied props. |
ref | Ref<HTMLDivElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormLabel
Label associated with a field by name or the surrounding FormField. Native label attributes are forwarded.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | No | — | Target field name; inherited from the surrounding FormField when omitted. |
htmlFor | string | No | — | ID of the native control labelled by this element. |
children | ReactNode | No | — | Content rendered inside the component. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
asChild | boolean | No | — | Render through one child element instead of the default element. The child must forward its ref and accept the supplied props. |
ref | Ref<HTMLLabelElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormControl
Native input validation integration. Use asChild with a single Input or TextareaInput; this does not add native validation to Select.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | — | Compose with one native-input-compatible child, such as Input or TextareaInput. Select is not a native validation control. |
type | HTMLInputTypeAttribute | No | — | Native control type. |
name | string | No | — | Native form submission field name. |
value | string | number | readonly string[] | No | — | Current native control value. Pair with onChange for controlled input. |
defaultValue | string | number | readonly string[] | No | — | Initial value used when the native control is uncontrolled. |
onChange | ChangeEventHandler<HTMLInputElement, HTMLInputElement> | No | — | Native input change handler. Read value or checked from event.currentTarget. |
disabled | boolean | No | — | Disables the native control. |
required | boolean | No | — | Requires a non-empty native control value during form validation. |
readOnly | boolean | No | — | Keeps the current value readable and focusable while preventing editing. |
placeholder | string | No | — | Text shown when the control has no value. |
children | ReactNode | No | — | Content rendered inside the component. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
ref | Ref<HTMLInputElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormMessage
Conditional validation feedback for a named field. Renders when match succeeds or forceMatch is true.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
children | ReactNode | Yes | — | Content rendered inside the component. |
name | string | No | — | Target field name; inherited from the surrounding FormField when omitted. |
match | "badInput" | "patternMismatch" | "rangeOverflow" | "rangeUnderflow" | "stepMismatch" | "tooLong" | "tooShort" | "typeMismatch" | "valid" | "valueMissing" | (((value: string, formData: FormData) => boolean) | ((value: string, formData: FormData) => Promise<boolean>)) | No | — | Native ValidityState key or custom matcher deciding when the message appears. |
forceMatch | boolean | No | — | Show the message independently of the current validation result. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
asChild | boolean | No | — | Render through one child element instead of the default element. The child must forward its ref and accept the supplied props. |
ref | Ref<HTMLSpanElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormDescription
Persistent field guidance associated with the native control. Does not accept match or forceMatch.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
children | ReactNode | Yes | — | Content rendered inside the component. |
name | string | No | — | Target field name; inherited from the surrounding FormField when omitted. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
asChild | boolean | No | — | Render through one child element instead of the default element. The child must forward its ref and accept the supplied props. |
ref | Ref<HTMLSpanElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormFieldset
A native fieldset for grouping related form controls, including group-level disabled state.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
disabled | boolean | No | — | Disables the native control. |
name | string | No | — | Native form submission field name. |
children | ReactNode | No | — | Content rendered inside the component. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
ref | Ref<HTMLFieldSetElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormLegend
A native legend naming a FormFieldset.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
children | ReactNode | No | — | Content rendered inside the component. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
ref | Ref<HTMLLegendElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormActions
A div arranging form action controls. Native div attributes and ref are forwarded.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
children | ReactNode | No | — | Content rendered inside the component. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
ref | Ref<HTMLDivElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormSubmit
A Radix submit button styled as a primary action. Use asChild to compose a custom submit control.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | — | Render through one child element instead of the default element. The child must forward its ref and accept the supplied props. |
type | "button" | "submit" | "reset" | No | "submit" | Native button behavior. With asChild, ensure the child retains type="submit". |
children | ReactNode | No | — | Content rendered inside the component. |
disabled | boolean | No | — | Disables the native control. |
onClick | MouseEventHandler<HTMLButtonElement> | No | — | Native activation handler. |
name | string | No | — | Native form submission field name. |
value | string | number | readonly string[] | No | — | Current native control value. Pair with onChange for controlled input. |
className | string | No | "" | Additional classes applied to the component’s documented root or control. |
id | string | No | — | DOM identifier for the rendered element. |
ref | Ref<HTMLButtonElement> | No | — | Ref to the component’s documented DOM element. |
No custom imperative methods. Event callbacks are listed as properties.
FormValidityState
Render-prop access to a native field’s validity state. Does not render an extra DOM element.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | No | — | Target field name; inherited from the surrounding FormField when omitted. |
children | (validity: ValidityState | undefined) => ReactNode | Yes | — | Render function receiving the native ValidityState or undefined before the control is available. |
No custom imperative methods. Event callbacks are listed as properties.