import { FormField } from '@/components/FormField';
<FormField
id="email"
label="Email Address"
type="email"
required
helpText="We'll never share your email"
error={errors.email}
inputProps={{
value: formData.email,
onChange: handleChange,
placeholder: "you@example.com",
autoComplete: "email"
}}
/><FormField
id="username"
label="Username"
type="text"
required
helpText="Choose a unique username"
error={errors.username}
inputProps={{
value: username,
onChange: (e) => setUsername(e.target.value),
placeholder: "johndoe"
}}
/><FormField
id="email"
label="Email Address"
type="email"
required
helpText="We'll never share your email"
error={errors.email}
inputProps={{
value: email,
onChange: (e) => setEmail(e.target.value),
placeholder: "you@example.com",
autoComplete: "email"
}}
/><FormField
id="password"
label="Password"
type="password"
required
helpText="Must be at least 8 characters"
error={errors.password}
inputProps={{
value: password,
onChange: (e) => setPassword(e.target.value),
placeholder: "Enter your password",
autoComplete: "current-password"
}}
/><FormField
id="amount"
label="Amount"
type="number"
required
helpText="Enter amount in USD"
error={errors.amount}
inputProps={{
value: amount,
onChange: (e) => setAmount(e.target.value),
placeholder: "0.00",
min: "0",
step: "0.01"
}}
/><FormField
id="description"
label="Description"
as="textarea"
required
helpText="Provide a detailed description"
error={errors.description}
inputProps={{
value: description,
onChange: (e) => setDescription(e.target.value),
placeholder: "Enter description...",
rows: 4
}}
/><FormField
id="document"
label="Upload Document"
helpText="PDF or CSV files only"
error={errors.document}
as="custom"
>
{(props) => (
<input
{...props}
type="file"
accept=".pdf,.csv"
onChange={handleFileChange}
/>
)}
</FormField><div className="form-field form-field--checkbox">
<input
id="accept-terms"
type="checkbox"
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
className="form-field__input"
aria-describedby="accept-terms-help"
aria-required="true"
/>
<label htmlFor="accept-terms" className="form-field__label">
I accept the Terms and Conditions
<span className="form-field__required" aria-label="required">*</span>
</label>
<p id="accept-terms-help" className="form-field__help">
You must accept to continue
</p>
</div><fieldset>
<legend className="form-field__label">
Payment Method
<span className="form-field__required" aria-label="required">*</span>
</legend>
<p className="form-field__help">Select your preferred payment method</p>
<div className="form-field form-field--radio">
<input
id="credit-card"
type="radio"
name="payment"
value="credit-card"
checked={payment === 'credit-card'}
onChange={(e) => setPayment(e.target.value)}
className="form-field__input"
/>
<label htmlFor="credit-card" className="form-field__label">
Credit Card
</label>
</div>
<div className="form-field form-field--radio">
<input
id="bank-transfer"
type="radio"
name="payment"
value="bank-transfer"
checked={payment === 'bank-transfer'}
onChange={(e) => setPayment(e.target.value)}
className="form-field__input"
/>
<label htmlFor="bank-transfer" className="form-field__label">
Bank Transfer
</label>
</div>
</fieldset>| Prop | Type | Required | Description |
|---|---|---|---|
id |
string |
✅ | Unique identifier for the field |
label |
string |
✅ | Visible label text |
type |
'text' | 'email' | 'password' | 'number' | 'tel' | 'url' |
❌ | Input type (default: 'text') |
as |
'input' | 'textarea' | 'custom' |
❌ | Element type (default: 'input') |
required |
boolean |
❌ | Shows asterisk and sets aria-required |
helpText |
string |
❌ | Instructional text below label |
error |
string |
❌ | Error message to display |
className |
string |
❌ | Additional CSS classes |
inputProps |
object |
❌ | Props passed to input/textarea |
const [errors, setErrors] = useState<Record<string, string>>({});
// Set error for specific field
setErrors({ ...errors, email: 'Please enter a valid email' });
// Use in FormField
<FormField
id="email"
error={errors.email}
// ...
/>function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
// Clear error when user starts typing
if (errors[name]) {
setErrors({ ...errors, [name]: '' });
}
}function handleBlur(field: string) {
const value = formData[field];
if (field === 'email' && !isValidEmail(value)) {
setErrors({ ...errors, email: 'Please enter a valid email' });
}
}
<FormField
id="email"
inputProps={{
value: formData.email,
onChange: handleChange,
onBlur: () => handleBlur('email')
}}
/>- ✅ Label is associated with input via
htmlFor/id - ✅ Required fields show asterisk with
aria-label="required" - ✅ Help text is linked via
aria-describedby - ✅ Error messages use
aria-invalidandaria-live="polite" - ✅ Focus rings are visible (2px solid accent)
- ✅ Keyboard navigation works (Tab/Shift+Tab)
- ✅ Color contrast meets WCAG AA (4.5:1)
<label>Email</label>
<input id="email" /><label htmlFor="email">Email</label>
<input id="email" /><label htmlFor="email">Email</label>
<input id="email" required /><label htmlFor="email">
Email
<span className="form-field__required" aria-label="required">*</span>
</label>
<input id="email" aria-required="true" />{error && <div>{error}</div>}
<input id="email" />{error && (
<div id="email-error" role="alert" aria-live="polite">
{error}
</div>
)}
<input id="email" aria-invalid={!!error} aria-describedby="email-error" />.form-field- Container for the entire field.form-field__label- Label element.form-field__required- Required asterisk.form-field__help- Help text.form-field__input- Input element.form-field__textarea- Textarea element.form-field__input--error- Error state for input.form-field__error- Error message container.form-field__error-icon- Error icon.form-field--checkbox- Checkbox variant.form-field--radio- Radio variant
--text- Primary text color--muted- Secondary text color--error- Error color--surface- Input background--border- Input border--accent- Focus/hover color--space-xs- 0.25rem--space-sm- 0.5rem--space-md- 0.75rem--space-lg- 1rem--space-xl- 1.5rem
- Tab through all fields - focus should be visible
- Use screen reader - all elements should be announced
- Submit with errors - errors should appear and be announced
- Fix errors - errors should clear
- Test with keyboard only - should be fully functional
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('shows error message', async () => {
const { rerender } = render(
<FormField
id="email"
label="Email"
error=""
inputProps={{ value: '', onChange: () => {} }}
/>
);
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
rerender(
<FormField
id="email"
label="Email"
error="Invalid email"
inputProps={{ value: '', onChange: () => {} }}
/>
);
expect(screen.getByRole('alert')).toHaveTextContent('Invalid email');
});For questions or issues:
- Check COMPONENT_SPECS.md for detailed documentation
- Review FORM_FIELD_STANDARDIZATION_SUMMARY.md for implementation details
- Refer to existing implementations in auth pages