FormControl
Groups a label, control, and helper text into one accessible field and shares their state through React context.
Groups a label, control, and helper text into one accessible field and shares their state through React context. Works standalone or inside a Form.
Architecture: FormControl renders a View and provides FormControlContext. It auto-generates an id via useId() (override with the id prop) and derives helperId = ${id}-helper and labelId = ${id}-label. Descendant controls read field state through useFormControl; FormLabel reads the id for htmlFor; FormHelperText registers its presence so the control points aria-describedby at it only while it is rendered - no dangling reference, no manual wiring. FormLabel registers too, giving a group control (a rating, which a for can't label) its labelId to name itself by.
Override chain
Field state resolves explicit prop → FormControl → Form → default. A prop set on the control always wins; otherwise FormControl fills it; otherwise the surrounding Form; otherwise the control's own default.
error and required are field-only - they are never read from Form.
Examples
Default
We'll never share it.
const args: Partial<FormControlProps> = { required: true, fullWidth: true } <FormControl {...args}> <Flex flexDirection="column" gap={1} alignItems="flex-start"> <FormLabel>Email</FormLabel> <TextInput variant="outlined" placeholder="[email protected]" /> <FormHelperText>We'll never share it.</FormHelperText> </Flex> </FormControl>
Error State
Enter a valid e-mail address.
const args: Partial<FormControlProps> = { error: true, required: true, fullWidth: true } <FormControl {...args}> <Flex flexDirection="column" gap={1} alignItems="flex-start"> <FormLabel>Email</FormLabel> <TextInput variant="outlined" value="not-an-email" onChange={() => {}} /> <FormHelperText>Enter a valid e-mail address.</FormHelperText> </Flex> </FormControl>