Controlled vs uncontrolled forms
How to choose and combine controlled and uncontrolled patterns with Rad UI form primitives.
Definitions
| Pattern | Who owns state | Typical props |
|---|---|---|
| Uncontrolled | the component | defaultValue, defaultChecked, defaultOpen |
| Controlled | your app | value, checked, open, onValueChange, onOpenChange |
Rad UI primitives support both. Pick one model per field and stay consistent through a form subtree.
When to use uncontrolled
- simple demos and low-risk settings screens
- native form submission with
nameattributes - fields that do not drive other UI on every keystroke
When to use controlled
- validation that depends on current value
- syncing multiple inputs (for example Select + TextArea)
- serializing state to URL, server, or global store
- imperative reset after submit
Switching modes
Do not flip between controlled and uncontrolled on the same instance. Mount a new component or keep value defined for the component's lifetime.
Avoid
Prefer
Composite controls
For CheckboxGroup, RadioGroup, Tabs, and ToggleGroup:
type="single"/ single selection → scalarvaluetype="multiple"→ arrayvalue- document whether empty string or
undefinedmeans "no selection" in your app layer
Forms checklist
- Each field is either controlled or uncontrolled for its full lifetime
- Submit handlers read the latest controlled state or native form data
- Labels are associated with inputs (
htmlFor/idor wrappinglabel) - Validation messages use
aria-invalidandrole="alert"where appropriate