Skip to content

FormControl

FormControl provides a complete solution for form field management with built-in validation states, accessibility features, and contextual styling. It includes label, helper text, and error/success/warning message components with automatic state management.

import {
FormControl,
FormControlLabel,
FormControlLabelText,
FormControlHelper,
FormControlHelperText,
FormControlError,
FormControlErrorIcon,
FormControlErrorText,
FormControlSuccess,
FormControlSuccessIcon,
FormControlSuccessText,
FormControlWarning,
FormControlWarningIcon,
FormControlWarningText,
useFormControl,
useFormControlOptional,
} from 'rnc-theme';
<FormControl>
<FormControlLabel>
<FormControlLabelText>Email Address</FormControlLabelText>
</FormControlLabel>
<TextInput placeholder="Enter your email" />
<FormControlHelper>
<FormControlHelperText>We'll never share your email</FormControlHelperText>
</FormControlHelper>
</FormControl>
PropTypeDefaultDescription
childrenReact.ReactNode-Form control content
idstring-Unique identifier (auto-generated if not provided)
stateFormControlState'default'Visual state (default, error, success, warning, disabled)
sizeFormControlSize'md'Size variant (sm, md, lg)
disabledbooleanfalseDisable all form control elements
requiredbooleanfalseMark field as required
styleStyleProp<ViewStyle>-Additional container styles
spacingkeyof Theme['spacing']'sm'Spacing between child elements
onStateChange(state: FormControlState) => void-Callback when state changes
PropTypeDefaultDescription
childrenReact.ReactNode-Label content
styleStyleProp<ViewStyle>-Additional label container styles
PropTypeDefaultDescription
childrenReact.ReactNode-Label text content
styleTextStyle-Additional text styles
variantkeyof Theme['typography']-Typography variant override
PropTypeDefaultDescription
childrenReact.ReactNode-Helper content
styleStyleProp<ViewStyle>-Additional helper container styles
PropTypeDefaultDescription
childrenReact.ReactNode-Helper text content
styleTextStyle-Additional text styles
variantkeyof Theme['typography']-Typography variant override

State Message Props (Error/Success/Warning)

Section titled “State Message Props (Error/Success/Warning)”
PropTypeDefaultDescription
childrenReact.ReactNode-Message content
styleStyleProp<ViewStyle>-Additional container styles
showWhenboolean-Control visibility manually
PropTypeDefaultDescription
iconReact.ReactNode-Custom icon component
styleStyleProp<ViewStyle>-Additional icon container styles
sizenumber16Icon size in pixels
PropTypeDefaultDescription
childrenReact.ReactNode-Text content
styleTextStyle-Additional text styles
variantkeyof Theme['typography']-Typography variant override
StateDescriptionUse Case
defaultNormal stateStandard form fields
errorError stateValidation failures
successSuccess stateSuccessful validation
warningWarning stateCaution messages
disabledDisabled stateInactive form fields
const LoginForm = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailError, setEmailError] = useState('');
const [passwordError, setPasswordError] = useState('');
const validateEmail = (value: string) => {
if (!value) {
setEmailError('Email is required');
return false;
}
if (!/\S+@\S+\.\S+/.test(value)) {
setEmailError('Please enter a valid email');
return false;
}
setEmailError('');
return true;
};
const validatePassword = (value: string) => {
if (!value) {
setPasswordError('Password is required');
return false;
}
if (value.length < 8) {
setPasswordError('Password must be at least 8 characters');
return false;
}
setPasswordError('');
return true;
};
return (
<VStack spacing="lg" padding="xl">
<FormControl
state={emailError ? 'error' : email ? 'success' : 'default'}
required
>
<FormControlLabel>
<FormControlLabelText>Email Address</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Enter your email"
value={email}
onChangeText={(value) => {
setEmail(value);
validateEmail(value);
}}
keyboardType="email-address"
autoCapitalize="none"
/>
{emailError ? (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>{emailError}</FormControlErrorText>
</FormControlError>
) : email ? (
<FormControlSuccess>
<FormControlSuccessIcon />
<FormControlSuccessText>Valid email address</FormControlSuccessText>
</FormControlSuccess>
) : (
<FormControlHelper>
<FormControlHelperText>We'll never share your email</FormControlHelperText>
</FormControlHelper>
)}
</FormControl>
<FormControl
state={passwordError ? 'error' : password.length >= 8 ? 'success' : 'default'}
required
>
<FormControlLabel>
<FormControlLabelText>Password</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Enter your password"
value={password}
onChangeText={(value) => {
setPassword(value);
validatePassword(value);
}}
secureTextEntry
/>
{passwordError ? (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>{passwordError}</FormControlErrorText>
</FormControlError>
) : password.length >= 8 ? (
<FormControlSuccess>
<FormControlSuccessIcon />
<FormControlSuccessText>Strong password</FormControlSuccessText>
</FormControlSuccess>
) : (
<FormControlHelper>
<FormControlHelperText>Password must be at least 8 characters</FormControlHelperText>
</FormControlHelper>
)}
</FormControl>
</VStack>
);
};
const RegistrationForm = () => {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
terms: false,
});
const [validationState, setValidationState] = useState({});
const validateField = (field: string, value: any) => {
let isValid = true;
let message = '';
switch (field) {
case 'username':
if (!value) {
isValid = false;
message = 'Username is required';
} else if (value.length < 3) {
isValid = false;
message = 'Username must be at least 3 characters';
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
isValid = false;
message = 'Username can only contain letters, numbers, and underscores';
}
break;
case 'email':
if (!value) {
isValid = false;
message = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(value)) {
isValid = false;
message = 'Please enter a valid email address';
}
break;
case 'password':
if (!value) {
isValid = false;
message = 'Password is required';
} else if (value.length < 8) {
isValid = false;
message = 'Password must be at least 8 characters';
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
isValid = false;
message = 'Password must contain uppercase, lowercase, and number';
}
break;
case 'confirmPassword':
if (!value) {
isValid = false;
message = 'Please confirm your password';
} else if (value !== formData.password) {
isValid = false;
message = 'Passwords do not match';
}
break;
}
setValidationState(prev => ({
...prev,
[field]: { isValid, message }
}));
return isValid;
};
const getFieldState = (field: string) => {
const validation = validationState[field];
if (!validation) return 'default';
if (!formData[field]) return 'default';
return validation.isValid ? 'success' : 'error';
};
return (
<VStack spacing="lg" padding="xl">
<FormControl
state={getFieldState('username')}
required
size="md"
>
<FormControlLabel>
<FormControlLabelText>Username</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Choose a username"
value={formData.username}
onChangeText={(value) => {
setFormData(prev => ({ ...prev, username: value }));
validateField('username', value);
}}
autoCapitalize="none"
/>
{validationState.username?.message ? (
getFieldState('username') === 'error' ? (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>{validationState.username.message}</FormControlErrorText>
</FormControlError>
) : (
<FormControlSuccess>
<FormControlSuccessIcon />
<FormControlSuccessText>Username is available</FormControlSuccessText>
</FormControlSuccess>
)
) : (
<FormControlHelper>
<FormControlHelperText>Choose a unique username (3+ characters)</FormControlHelperText>
</FormControlHelper>
)}
</FormControl>
<FormControl
state={getFieldState('email')}
required
>
<FormControlLabel>
<FormControlLabelText>Email Address</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Enter your email"
value={formData.email}
onChangeText={(value) => {
setFormData(prev => ({ ...prev, email: value }));
validateField('email', value);
}}
keyboardType="email-address"
autoCapitalize="none"
/>
{validationState.email?.message && getFieldState('email') === 'error' && (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>{validationState.email.message}</FormControlErrorText>
</FormControlError>
)}
</FormControl>
<FormControl
state={getFieldState('password')}
required
>
<FormControlLabel>
<FormControlLabelText>Password</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Create a password"
value={formData.password}
onChangeText={(value) => {
setFormData(prev => ({ ...prev, password: value }));
validateField('password', value);
if (formData.confirmPassword) {
validateField('confirmPassword', formData.confirmPassword);
}
}}
secureTextEntry
/>
{validationState.password?.message ? (
getFieldState('password') === 'error' ? (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>{validationState.password.message}</FormControlErrorText>
</FormControlError>
) : (
<FormControlSuccess>
<FormControlSuccessIcon />
<FormControlSuccessText>Strong password</FormControlSuccessText>
</FormControlSuccess>
)
) : (
<FormControlHelper>
<FormControlHelperText>
Include uppercase, lowercase, and numbers (8+ characters)
</FormControlHelperText>
</FormControlHelper>
)}
</FormControl>
<FormControl
state={getFieldState('confirmPassword')}
required
>
<FormControlLabel>
<FormControlLabelText>Confirm Password</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Confirm your password"
value={formData.confirmPassword}
onChangeText={(value) => {
setFormData(prev => ({ ...prev, confirmPassword: value }));
validateField('confirmPassword', value);
}}
secureTextEntry
/>
{validationState.confirmPassword?.message && getFieldState('confirmPassword') === 'error' && (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>{validationState.confirmPassword.message}</FormControlErrorText>
</FormControlError>
)}
</FormControl>
</VStack>
);
};
<VStack spacing="xl" padding="lg">
<FormControl size="sm">
<FormControlLabel>
<FormControlLabelText>Small Size</FormControlLabelText>
</FormControlLabel>
<TextInput placeholder="Small input" />
<FormControlHelper>
<FormControlHelperText>Small helper text</FormControlHelperText>
</FormControlHelper>
</FormControl>
<FormControl size="md">
<FormControlLabel>
<FormControlLabelText>Medium Size (Default)</FormControlLabelText>
</FormControlLabel>
<TextInput placeholder="Medium input" />
<FormControlHelper>
<FormControlHelperText>Medium helper text</FormControlHelperText>
</FormControlHelper>
</FormControl>
<FormControl size="lg">
<FormControlLabel>
<FormControlLabelText>Large Size</FormControlLabelText>
</FormControlLabel>
<TextInput placeholder="Large input" />
<FormControlHelper>
<FormControlHelperText>Large helper text</FormControlHelperText>
</FormControlHelper>
</FormControl>
</VStack>
const PasswordChangeForm = () => {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const isWeakPassword = (password: string) => {
return password.length >= 6 && password.length < 8;
};
return (
<VStack spacing="lg">
<FormControl>
<FormControlLabel>
<FormControlLabelText>Current Password</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Enter current password"
value={currentPassword}
onChangeText={setCurrentPassword}
secureTextEntry
/>
</FormControl>
<FormControl
state={
newPassword && isWeakPassword(newPassword) ? 'warning' :
newPassword && newPassword.length >= 8 ? 'success' :
'default'
}
required
>
<FormControlLabel>
<FormControlLabelText>New Password</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Enter new password"
value={newPassword}
onChangeText={setNewPassword}
secureTextEntry
/>
{isWeakPassword(newPassword) ? (
<FormControlWarning>
<FormControlWarningIcon />
<FormControlWarningText>
Password is acceptable but could be stronger
</FormControlWarningText>
</FormControlWarning>
) : newPassword.length >= 8 ? (
<FormControlSuccess>
<FormControlSuccessIcon />
<FormControlSuccessText>Strong password!</FormControlSuccessText>
</FormControlSuccess>
) : (
<FormControlHelper>
<FormControlHelperText>
Minimum 8 characters recommended
</FormControlHelperText>
</FormControlHelper>
)}
</FormControl>
<FormControl
state={
confirmPassword && confirmPassword !== newPassword ? 'error' :
confirmPassword && confirmPassword === newPassword ? 'success' :
'default'
}
required
>
<FormControlLabel>
<FormControlLabelText>Confirm New Password</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Confirm new password"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
/>
{confirmPassword && confirmPassword !== newPassword && (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>Passwords do not match</FormControlErrorText>
</FormControlError>
)}
{confirmPassword && confirmPassword === newPassword && (
<FormControlSuccess>
<FormControlSuccessIcon />
<FormControlSuccessText>Passwords match</FormControlSuccessText>
</FormControlSuccess>
)}
</FormControl>
</VStack>
);
};
const useFormValidation = (initialValues: Record<string, any>) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const setValue = (field: string, value: any) => {
setValues(prev => ({ ...prev, [field]: value }));
if (touched[field]) {
validateField(field, value);
}
};
const setTouched = (field: string) => {
setTouched(prev => ({ ...prev, [field]: true }));
validateField(field, values[field]);
};
const validateField = (field: string, value: any) => {
// Add your validation logic here
// This is a simplified example
let error = '';
if (field === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
error = 'Invalid email format';
}
setErrors(prev => ({ ...prev, [field]: error }));
return !error;
};
const getFieldProps = (field: string) => ({
value: values[field] || '',
onChangeText: (value: string) => setValue(field, value),
onBlur: () => setTouched(field),
});
const getFormControlProps = (field: string) => ({
state: errors[field] ? 'error' as const :
values[field] && touched[field] ? 'success' as const :
'default' as const,
});
return {
values,
errors,
touched,
getFieldProps,
getFormControlProps,
validateField,
setTouched,
};
};
// Usage
const MyForm = () => {
const { getFieldProps, getFormControlProps, errors } = useFormValidation({
email: '',
password: '',
});
return (
<VStack spacing="md">
<FormControl {...getFormControlProps('email')} required>
<FormControlLabel>
<FormControlLabelText>Email</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Enter email"
{...getFieldProps('email')}
keyboardType="email-address"
/>
{errors.email && (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>{errors.email}</FormControlErrorText>
</FormControlError>
)}
</FormControl>
<FormControl {...getFormControlProps('password')} required>
<FormControlLabel>
<FormControlLabelText>Password</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Enter password"
{...getFieldProps('password')}
secureTextEntry
/>
{errors.password && (
<FormControlError>
<FormControlErrorIcon />
<FormControlErrorText>{errors.password}</FormControlErrorText>
</FormControlError>
)}
</FormControl>
</VStack>
);
};
const DynamicFormControl = () => {
const [fieldState, setFieldState] = useState<FormControlState>('default');
const [inputValue, setInputValue] = useState('');
// Simulate real-time validation
useEffect(() => {
const timer = setTimeout(() => {
if (!inputValue) {
setFieldState('default');
} else if (inputValue.length < 3) {
setFieldState('error');
} else if (inputValue.length < 6) {
setFieldState('warning');
} else {
setFieldState('success');
}
}, 500);
return () => clearTimeout(timer);
}, [inputValue]);
return (
<FormControl
state={fieldState}
onStateChange={(newState) => {
console.log('State changed to:', newState);
}}
>
<FormControlLabel>
<FormControlLabelText>Dynamic Validation</FormControlLabelText>
</FormControlLabel>
<TextInput
placeholder="Type to see validation states"
value={inputValue}
onChangeText={setInputValue}
/>
<FormControlError showWhen={fieldState === 'error'}>
<FormControlErrorIcon />
<FormControlErrorText>Too short (minimum 3 characters)</FormControlErrorText>
</FormControlError>
<FormControlWarning showWhen={fieldState === 'warning'}>
<FormControlWarningIcon />
<FormControlWarningText>Could be longer (6+ recommended)</FormControlWarningText>
</FormControlWarning>
<FormControlSuccess showWhen={fieldState === 'success'}>
<FormControlSuccessIcon />
<FormControlSuccessText>Perfect length!</FormControlSuccessText>
</FormControlSuccess>
<FormControlHelper>
<FormControlHelperText>
{fieldState === 'default' && 'Start typing to see validation'}
</FormControlHelperText>
</FormControlHelper>
</FormControl>
);
};

Validation Timing

  • Validate on blur for better UX, not on every keystroke
  • Show success states only after user interaction
  • Use warning states for non-critical issues

Message Hierarchy

  • Error messages take priority over success/warning
  • Helper text is hidden when status messages are shown
  • Keep messages concise and actionable

Performance

  • Use useFormControlOptional for optional form control context
  • Memoize validation functions to prevent unnecessary re-renders
  • Consider debouncing for real-time validation