17 min read
Forms & CTAs
Optimize forms and call-to-action elements with proven techniques for higher completion rates and click-through rates.
Form Optimization Fundamentals
Forms are where conversions happen—or don't. Every unnecessary field, confusing label, or friction point costs you conversions.
code
Form Field Impact on Conversion:
Fields │ Completion Rate
──────────┼──────────────────
3 fields │ 25%
5 fields │ 20%
7 fields │ 15%
10+ fields│ 10%
Each additional field reduces conversions by ~4-5%The Golden Rule: Only ask for what you absolutely need right now. You can always collect more data later through progressive profiling.
High-Converting Form Patterns
Single Column Layout
code
// Optimal single-column form layout
function OptimizedLeadForm() {
const [formData, setFormData] = useState({
email: '',
firstName: '',
company: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate
const newErrors = validateForm(formData);
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setIsSubmitting(true);
// Track form submission attempt
trackEvent('form_submit_attempt', {
form_name: 'lead_capture',
fields_completed: Object.keys(formData).length
});
try {
await submitForm(formData);
trackEvent('form_submit_success', { form_name: 'lead_capture' });
window.location.href = '/thank-you';
} catch (error) {
trackEvent('form_submit_error', { form_name: 'lead_capture' });
setErrors({ submit: 'Something went wrong. Please try again.' });
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="lead-form">
{/* Email - most important, comes first */}
<FormField
type="email"
name="email"
label="Work Email"
placeholder="you@company.com"
value={formData.email}
onChange={(value) => setFormData({ ...formData, email: value })}
error={errors.email}
autoComplete="email"
required
/>
{/* First name only - not full name */}
<FormField
type="text"
name="firstName"
label="First Name"
placeholder="Jane"
value={formData.firstName}
onChange={(value) => setFormData({ ...formData, firstName: value })}
error={errors.firstName}
autoComplete="given-name"
required
/>
{/* Company - optional with clear indicator */}
<FormField
type="text"
name="company"
label="Company"
labelSuffix="(optional)"
placeholder="Acme Inc"
value={formData.company}
onChange={(value) => setFormData({ ...formData, company: value })}
autoComplete="organization"
/>
{/* Clear CTA with loading state */}
<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? 'Sending...' : 'Get Free Access'}
</button>
{/* Trust signals */}
<p className="form-footer">
<LockIcon /> Your data is secure. We never share your information.
</p>
{errors.submit && (
<p className="form-error">{errors.submit}</p>
)}
</form>
);
}
// Reusable form field component
function FormField({
type,
name,
label,
labelSuffix,
placeholder,
value,
onChange,
error,
autoComplete,
required
}: FormFieldProps) {
return (
<div className={`form-field ${error ? 'has-error' : ''}`}>
<label htmlFor={name}>
{label}
{labelSuffix && <span className="label-suffix">{labelSuffix}</span>}
{required && <span className="required">*</span>}
</label>
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
autoComplete={autoComplete}
required={required}
aria-invalid={!!error}
aria-describedby={error ? `${name}-error` : undefined}
/>
{error && (
<span id={`${name}-error`} className="field-error" role="alert">
{error}
</span>
)}
</div>
);
}Multi-Step Form
code
interface MultiStepFormProps {
steps: FormStep[];
onComplete: (data: Record<string, any>) => void;
}
function MultiStepForm({ steps, onComplete }: MultiStepFormProps) {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState<Record<string, any>>({});
const [direction, setDirection] = useState<'forward' | 'backward'>('forward');
const currentStepConfig = steps[currentStep];
const isLastStep = currentStep === steps.length - 1;
const progress = ((currentStep + 1) / steps.length) * 100;
const handleNext = async () => {
// Validate current step
const stepErrors = validateStep(currentStepConfig, formData);
if (Object.keys(stepErrors).length > 0) {
return;
}
// Track step completion
trackEvent('form_step_complete', {
step: currentStep + 1,
step_name: currentStepConfig.name
});
if (isLastStep) {
onComplete(formData);
} else {
setDirection('forward');
setCurrentStep(currentStep + 1);
}
};
const handleBack = () => {
setDirection('backward');
setCurrentStep(currentStep - 1);
};
return (
<div className="multi-step-form">
{/* Progress indicator */}
<div className="progress-container">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progress}%` }}
/>
</div>
<p className="progress-text">
Step {currentStep + 1} of {steps.length}
</p>
</div>
{/* Step indicator */}
<div className="step-indicators">
{steps.map((step, index) => (
<div
key={step.name}
className={`step-indicator ${
index < currentStep ? 'completed' :
index === currentStep ? 'active' : 'pending'
}`}
>
<span className="step-number">
{index < currentStep ? <CheckIcon /> : index + 1}
</span>
<span className="step-label">{step.label}</span>
</div>
))}
</div>
{/* Step content */}
<div className={`step-content ${direction}`}>
<h2>{currentStepConfig.title}</h2>
<p>{currentStepConfig.description}</p>
<div className="step-fields">
{currentStepConfig.fields.map(field => (
<FormField
key={field.name}
{...field}
value={formData[field.name] || ''}
onChange={(value) => setFormData({
...formData,
[field.name]: value
})}
/>
))}
</div>
</div>
{/* Navigation */}
<div className="step-navigation">
{currentStep > 0 && (
<button
type="button"
onClick={handleBack}
className="btn-secondary"
>
Back
</button>
)}
<button
type="button"
onClick={handleNext}
className="btn-primary"
>
{isLastStep ? 'Complete' : 'Continue'}
</button>
</div>
</div>
);
}
// Example usage
const signupSteps: FormStep[] = [
{
name: 'account',
label: 'Account',
title: 'Create your account',
description: 'Enter your email and create a password',
fields: [
{ name: 'email', type: 'email', label: 'Email', required: true },
{ name: 'password', type: 'password', label: 'Password', required: true }
]
},
{
name: 'profile',
label: 'Profile',
title: 'Tell us about yourself',
description: 'Help us personalize your experience',
fields: [
{ name: 'firstName', type: 'text', label: 'First Name', required: true },
{ name: 'company', type: 'text', label: 'Company', required: false },
{ name: 'role', type: 'select', label: 'Your Role', options: ['Marketing', 'Sales', 'Engineering', 'Other'] }
]
},
{
name: 'preferences',
label: 'Setup',
title: 'Set up your workspace',
description: 'Choose your preferences',
fields: [
{ name: 'teamSize', type: 'select', label: 'Team Size', options: ['Just me', '2-10', '11-50', '50+'] },
{ name: 'useCase', type: 'select', label: 'Primary Use Case', options: ['Analytics', 'Attribution', 'Reporting', 'All'] }
]
}
];Inline Validation
code
// Real-time validation with clear feedback
class FormValidator {
private rules: Map<string, ValidationRule[]> = new Map();
addRule(fieldName: string, rule: ValidationRule) {
if (!this.rules.has(fieldName)) {
this.rules.set(fieldName, []);
}
this.rules.get(fieldName)!.push(rule);
}
validate(fieldName: string, value: string): ValidationResult {
const fieldRules = this.rules.get(fieldName) || [];
const errors: string[] = [];
for (const rule of fieldRules) {
const result = rule.validate(value);
if (!result.valid) {
errors.push(result.message);
}
}
return {
valid: errors.length === 0,
errors
};
}
validateAll(formData: Record<string, string>): Record<string, string[]> {
const allErrors: Record<string, string[]> = {};
for (const [fieldName, value] of Object.entries(formData)) {
const result = this.validate(fieldName, value);
if (!result.valid) {
allErrors[fieldName] = result.errors;
}
}
return allErrors;
}
}
// Common validation rules
const validationRules = {
required: (message = 'This field is required'): ValidationRule => ({
validate: (value: string) => ({
valid: value.trim().length > 0,
message
})
}),
email: (message = 'Please enter a valid email'): ValidationRule => ({
validate: (value: string) => ({
valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message
})
}),
minLength: (min: number, message?: string): ValidationRule => ({
validate: (value: string) => ({
valid: value.length >= min,
message: message || `Must be at least ${min} characters`
})
}),
passwordStrength: (): ValidationRule => ({
validate: (value: string) => {
const checks = {
length: value.length >= 8,
uppercase: /[A-Z]/.test(value),
lowercase: /[a-z]/.test(value),
number: /\d/.test(value)
};
const strength = Object.values(checks).filter(Boolean).length;
return {
valid: strength >= 3,
message: strength < 3
? 'Password needs uppercase, lowercase, and numbers'
: ''
};
}
}),
phone: (message = 'Please enter a valid phone number'): ValidationRule => ({
validate: (value: string) => ({
valid: /^\+?[\d\s\-()]{10,}$/.test(value.replace(/\s/g, '')),
message
})
}),
url: (message = 'Please enter a valid URL'): ValidationRule => ({
validate: (value: string) => {
try {
new URL(value.startsWith('http') ? value : `https://${value}`);
return { valid: true, message: '' };
} catch {
return { valid: false, message };
}
}
})
};
// React hook for form validation
function useFormValidation(initialValues: Record<string, string>) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const validator = useMemo(() => {
const v = new FormValidator();
// Set up rules
v.addRule('email', validationRules.required());
v.addRule('email', validationRules.email());
v.addRule('password', validationRules.required());
v.addRule('password', validationRules.passwordStrength());
return v;
}, []);
const handleChange = (name: string, value: string) => {
setValues(prev => ({ ...prev, [name]: value }));
// Validate on change if field was touched
if (touched[name]) {
const result = validator.validate(name, value);
setErrors(prev => ({
...prev,
[name]: result.errors[0] || ''
}));
}
};
const handleBlur = (name: string) => {
setTouched(prev => ({ ...prev, [name]: true }));
// Validate on blur
const result = validator.validate(name, values[name]);
setErrors(prev => ({
...prev,
[name]: result.errors[0] || ''
}));
};
const validateAll = () => {
const allErrors = validator.validateAll(values);
const firstErrors: Record<string, string> = {};
for (const [field, fieldErrors] of Object.entries(allErrors)) {
firstErrors[field] = fieldErrors[0] || '';
}
setErrors(firstErrors);
setTouched(Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {}));
return Object.keys(allErrors).length === 0;
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
validateAll,
isValid: Object.values(errors).every(e => !e)
};
}Form Analytics
code
class FormAnalytics {
private formId: string;
private startTime: number = 0;
private fieldInteractions: Map<string, FieldInteraction> = new Map();
constructor(formId: string) {
this.formId = formId;
this.init();
}
private init() {
const form = document.getElementById(this.formId) as HTMLFormElement;
if (!form) return;
this.startTime = Date.now();
// Track field focus
form.querySelectorAll('input, textarea, select').forEach(field => {
const name = field.getAttribute('name') || field.id;
if (!name) return;
this.fieldInteractions.set(name, {
name,
focusCount: 0,
focusTime: 0,
blurCount: 0,
lastFocusTime: 0,
corrections: 0,
completed: false
});
field.addEventListener('focus', () => this.handleFocus(name));
field.addEventListener('blur', () => this.handleBlur(name, field as HTMLInputElement));
field.addEventListener('input', () => this.handleInput(name, field as HTMLInputElement));
});
// Track form submission
form.addEventListener('submit', () => this.handleSubmit());
// Track abandonment
window.addEventListener('beforeunload', () => {
if (!this.submitted) {
this.handleAbandonment();
}
});
}
private submitted = false;
private handleFocus(fieldName: string) {
const interaction = this.fieldInteractions.get(fieldName);
if (!interaction) return;
interaction.focusCount++;
interaction.lastFocusTime = Date.now();
this.track('field_focus', {
field: fieldName,
focus_count: interaction.focusCount
});
}
private handleBlur(fieldName: string, field: HTMLInputElement) {
const interaction = this.fieldInteractions.get(fieldName);
if (!interaction) return;
interaction.blurCount++;
if (interaction.lastFocusTime) {
interaction.focusTime += Date.now() - interaction.lastFocusTime;
}
interaction.completed = !!field.value;
this.track('field_blur', {
field: fieldName,
time_spent: interaction.focusTime,
completed: interaction.completed,
focus_count: interaction.focusCount
});
}
private handleInput(fieldName: string, field: HTMLInputElement) {
const interaction = this.fieldInteractions.get(fieldName);
if (!interaction) return;
// Detect corrections (value got shorter)
const currentLength = field.value.length;
const previousLength = interaction.previousLength || 0;
if (currentLength < previousLength) {
interaction.corrections++;
}
interaction.previousLength = currentLength;
}
private handleSubmit() {
this.submitted = true;
const totalTime = Date.now() - this.startTime;
const fieldData = Array.from(this.fieldInteractions.values());
this.track('form_submit', {
form_id: this.formId,
total_time: totalTime,
fields_completed: fieldData.filter(f => f.completed).length,
total_fields: fieldData.length,
field_interactions: fieldData.map(f => ({
field: f.name,
focus_count: f.focusCount,
time_spent: f.focusTime,
corrections: f.corrections
}))
});
}
private handleAbandonment() {
const fieldData = Array.from(this.fieldInteractions.values());
const lastInteractedField = [...this.fieldInteractions.entries()]
.filter(([_, v]) => v.focusCount > 0)
.sort((a, b) => b[1].lastFocusTime - a[1].lastFocusTime)[0];
this.track('form_abandon', {
form_id: this.formId,
time_on_form: Date.now() - this.startTime,
last_field: lastInteractedField?.[0] || 'none',
fields_completed: fieldData.filter(f => f.completed).length,
total_fields: fieldData.length
});
}
private track(event: string, data: Record<string, any>) {
// GA4
window.gtag?.('event', event, data);
// Backend
fetch('/api/analytics/form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, ...data, timestamp: Date.now() }),
keepalive: true
});
}
}
// Initialize for all forms
document.querySelectorAll('form[data-track]').forEach(form => {
new FormAnalytics(form.id);
});CTA Button Optimization
Button Copy Best Practices
code
const ctaCopyGuidelines = {
// Use action verbs
actionVerbs: {
good: ['Get', 'Start', 'Try', 'Download', 'Join', 'Unlock', 'Claim'],
avoid: ['Submit', 'Click Here', 'Enter', 'Send']
},
// Be specific about the outcome
specificity: {
good: [
'Get My Free Report',
'Start 14-Day Trial',
'Download the Guide',
'Join 10,000+ Marketers'
],
avoid: [
'Submit',
'Click Here',
'Continue',
'Next'
]
},
// Use first person (increases ownership)
firstPerson: {
good: 'Start My Free Trial',
lessgood: 'Start Your Free Trial',
data: '+90% CTR improvement in studies'
},
// Reduce perceived risk
riskReduction: [
'No credit card required',
'Free forever',
'Cancel anytime',
'Instant access',
'30-day money-back guarantee'
]
};
// CTA A/B test examples with results
const ctaTestResults = [
{
control: 'Submit',
variant: 'Get My Free Quote',
lift: '+320%',
context: 'Insurance quote form'
},
{
control: 'Sign Up',
variant: 'Create My Account',
lift: '+31%',
context: 'SaaS signup'
},
{
control: 'Download',
variant: 'Send Me the Guide',
lift: '+45%',
context: 'Lead magnet'
},
{
control: 'Start Free Trial',
variant: 'Start My Free Trial',
lift: '+90%',
context: 'Software trial'
},
{
control: 'Buy Now',
variant: 'Add to Cart',
lift: '+17%',
context: 'E-commerce PDP'
}
];Button Design System
code
// CTA Button component with variants
interface CTAButtonProps {
variant: 'primary' | 'secondary' | 'ghost';
size: 'small' | 'medium' | 'large';
fullWidth?: boolean;
loading?: boolean;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
children: React.ReactNode;
onClick?: () => void;
type?: 'button' | 'submit';
}
function CTAButton({
variant = 'primary',
size = 'medium',
fullWidth = false,
loading = false,
icon,
iconPosition = 'right',
children,
onClick,
type = 'button'
}: CTAButtonProps) {
const baseClasses = `
cta-button
cta-${variant}
cta-${size}
${fullWidth ? 'cta-full-width' : ''}
${loading ? 'cta-loading' : ''}
`.trim();
return (
<button
type={type}
className={baseClasses}
onClick={onClick}
disabled={loading}
>
{loading && <Spinner className="cta-spinner" />}
{!loading && icon && iconPosition === 'left' && (
<span className="cta-icon cta-icon-left">{icon}</span>
)}
<span className="cta-text">{children}</span>
{!loading && icon && iconPosition === 'right' && (
<span className="cta-icon cta-icon-right">{icon}</span>
)}
</button>
);
}
// Button styles
const buttonStyles = `
/* Primary CTA - highest visibility */
.cta-primary {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
border: none;
box-shadow: 0 4px 14px 0 rgba(37, 99, 235, 0.4);
transition: all 0.2s ease;
}
.cta-primary:hover {
background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%);
box-shadow: 0 6px 20px 0 rgba(37, 99, 235, 0.5);
transform: translateY(-2px);
}
.cta-primary:active {
transform: translateY(0);
box-shadow: 0 2px 10px 0 rgba(37, 99, 235, 0.4);
}
/* Secondary CTA */
.cta-secondary {
background: white;
color: #2563eb;
border: 2px solid #2563eb;
box-shadow: none;
}
.cta-secondary:hover {
background: #f0f7ff;
}
/* Ghost CTA */
.cta-ghost {
background: transparent;
color: #2563eb;
border: none;
text-decoration: underline;
}
/* Sizes */
.cta-small {
padding: 8px 16px;
font-size: 14px;
border-radius: 6px;
}
.cta-medium {
padding: 12px 24px;
font-size: 16px;
border-radius: 8px;
}
.cta-large {
padding: 16px 32px;
font-size: 18px;
border-radius: 10px;
}
/* Full width */
.cta-full-width {
width: 100%;
justify-content: center;
}
/* Loading state */
.cta-loading {
opacity: 0.8;
cursor: not-allowed;
}
.cta-spinner {
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Icon positioning */
.cta-button {
display: inline-flex;
align-items: center;
font-weight: 600;
cursor: pointer;
}
.cta-icon-left {
margin-right: 8px;
}
.cta-icon-right {
margin-left: 8px;
}
/* Mobile touch targets */
@media (max-width: 768px) {
.cta-button {
min-height: 48px; /* Minimum touch target */
}
}
`;CTA Placement Heatmap
code
// Track CTA visibility and engagement
class CTATracker {
private ctas: Map<string, CTAMetrics> = new Map();
constructor() {
this.initTracking();
}
private initTracking() {
// Track all CTAs
document.querySelectorAll('[data-cta]').forEach(cta => {
const ctaId = cta.getAttribute('data-cta') || cta.id;
if (!ctaId) return;
this.ctas.set(ctaId, {
id: ctaId,
views: 0,
clicks: 0,
timeVisible: 0,
firstSeen: null,
position: this.getPosition(cta as HTMLElement)
});
// Visibility tracking
this.observeVisibility(cta as HTMLElement, ctaId);
// Click tracking
cta.addEventListener('click', () => this.handleClick(ctaId));
});
}
private observeVisibility(element: HTMLElement, ctaId: string) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
const metrics = this.ctas.get(ctaId);
if (!metrics) return;
if (entry.isIntersecting) {
if (!metrics.firstSeen) {
metrics.firstSeen = Date.now();
this.track('cta_view', {
cta_id: ctaId,
position: metrics.position,
time_to_view: performance.now()
});
}
metrics.views++;
metrics.visibleStart = Date.now();
} else if (metrics.visibleStart) {
metrics.timeVisible += Date.now() - metrics.visibleStart;
metrics.visibleStart = undefined;
}
});
},
{ threshold: 0.5 }
);
observer.observe(element);
}
private handleClick(ctaId: string) {
const metrics = this.ctas.get(ctaId);
if (!metrics) return;
metrics.clicks++;
this.track('cta_click', {
cta_id: ctaId,
position: metrics.position,
views_before_click: metrics.views,
time_visible: metrics.timeVisible,
time_to_click: metrics.firstSeen ? Date.now() - metrics.firstSeen : 0
});
}
private getPosition(element: HTMLElement): string {
const rect = element.getBoundingClientRect();
const scrollY = window.scrollY;
const viewportHeight = window.innerHeight;
const absoluteTop = rect.top + scrollY;
const pageHeight = document.body.scrollHeight;
if (absoluteTop < viewportHeight) {
return 'above_fold';
} else if (absoluteTop < pageHeight * 0.5) {
return 'upper_half';
} else {
return 'lower_half';
}
}
private track(event: string, data: Record<string, any>) {
window.gtag?.('event', event, data);
}
getReport(): CTAReport[] {
return Array.from(this.ctas.values()).map(metrics => ({
id: metrics.id,
position: metrics.position,
views: metrics.views,
clicks: metrics.clicks,
ctr: metrics.views > 0 ? (metrics.clicks / metrics.views * 100).toFixed(2) + '%' : '0%',
avgTimeVisible: metrics.views > 0 ? Math.round(metrics.timeVisible / metrics.views) : 0
}));
}
}Smart Form Defaults
code
// Pre-fill forms with smart defaults
class SmartFormDefaults {
private utm: Record<string, string> = {};
private referrer: string = '';
constructor() {
this.captureContext();
}
private captureContext() {
// Capture UTM parameters
const params = new URLSearchParams(window.location.search);
['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(param => {
const value = params.get(param);
if (value) this.utm[param] = value;
});
// Capture referrer
this.referrer = document.referrer;
// Store for later use
this.persistContext();
}
private persistContext() {
sessionStorage.setItem('form_context', JSON.stringify({
utm: this.utm,
referrer: this.referrer,
landingPage: window.location.href,
timestamp: Date.now()
}));
}
applyDefaults(form: HTMLFormElement) {
// Auto-fill hidden fields
const hiddenFields = {
utm_source: this.utm.utm_source,
utm_medium: this.utm.utm_medium,
utm_campaign: this.utm.utm_campaign,
referrer: this.referrer,
landing_page: window.location.pathname
};
Object.entries(hiddenFields).forEach(([name, value]) => {
if (!value) return;
let field = form.querySelector(`input[name="${name}"]`) as HTMLInputElement;
if (!field) {
field = document.createElement('input');
field.type = 'hidden';
field.name = name;
form.appendChild(field);
}
field.value = value;
});
// Prefill from localStorage (returning users)
const savedData = localStorage.getItem('user_prefill');
if (savedData) {
const data = JSON.parse(savedData);
if (data.email) {
const emailField = form.querySelector('input[type="email"]') as HTMLInputElement;
if (emailField) emailField.value = data.email;
}
if (data.firstName) {
const nameField = form.querySelector('input[name="firstName"]') as HTMLInputElement;
if (nameField) nameField.value = data.firstName;
}
}
// Country/timezone detection
this.detectLocation(form);
}
private async detectLocation(form: HTMLFormElement) {
const countryField = form.querySelector('select[name="country"]') as HTMLSelectElement;
if (!countryField) return;
try {
// Use timezone to guess country
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const countryCode = this.timezoneToCountry(timezone);
if (countryCode) {
const option = countryField.querySelector(`option[value="${countryCode}"]`);
if (option) {
countryField.value = countryCode;
}
}
} catch (e) {
// Fail silently
}
}
private timezoneToCountry(timezone: string): string | null {
const mapping: Record<string, string> = {
'America/New_York': 'US',
'America/Los_Angeles': 'US',
'America/Chicago': 'US',
'Europe/London': 'GB',
'Europe/Paris': 'FR',
'Europe/Berlin': 'DE',
'Asia/Tokyo': 'JP',
'Asia/Shanghai': 'CN',
'Australia/Sydney': 'AU'
// ... more mappings
};
return mapping[timezone] || null;
}
}
// Initialize
const smartDefaults = new SmartFormDefaults();
document.querySelectorAll('form').forEach(form => {
smartDefaults.applyDefaults(form);
});Form Accessibility
code
// Accessible form component
function AccessibleForm({ children, onSubmit, ariaLabel }: AccessibleFormProps) {
const [announcements, setAnnouncements] = useState<string[]>([]);
const formRef = useRef<HTMLFormElement>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const errors = validateForm(formRef.current!);
if (errors.length > 0) {
// Announce errors to screen readers
setAnnouncements([
`Form has ${errors.length} error${errors.length > 1 ? 's' : ''}. `,
...errors.map(e => e.message)
]);
// Focus first error field
const firstErrorField = formRef.current?.querySelector('[aria-invalid="true"]');
if (firstErrorField) {
(firstErrorField as HTMLElement).focus();
}
return;
}
await onSubmit(e);
};
return (
<>
{/* Live region for announcements */}
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcements.join(' ')}
</div>
<form
ref={formRef}
onSubmit={handleSubmit}
aria-label={ariaLabel}
noValidate // Use custom validation
>
{children}
</form>
</>
);
}
// Accessible form field
function AccessibleField({
id,
label,
type,
error,
description,
required,
...props
}: AccessibleFieldProps) {
const errorId = `${id}-error`;
const descriptionId = `${id}-description`;
const ariaDescribedBy = [
description ? descriptionId : null,
error ? errorId : null
].filter(Boolean).join(' ') || undefined;
return (
<div className="form-field">
<label htmlFor={id}>
{label}
{required && (
<span aria-hidden="true" className="required-indicator">*</span>
)}
{required && (
<span className="sr-only">(required)</span>
)}
</label>
{description && (
<p id={descriptionId} className="field-description">
{description}
</p>
)}
<input
id={id}
type={type}
aria-invalid={!!error}
aria-describedby={ariaDescribedBy}
aria-required={required}
{...props}
/>
{error && (
<p id={errorId} className="field-error" role="alert">
<ErrorIcon aria-hidden="true" />
{error}
</p>
)}
</div>
);
}
// Screen reader only styles
const srOnlyStyles = `
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
`;Form & CTA Testing Checklist
code
## Form Optimization Checklist
### Field Reduction
□ Remove non-essential fields
□ Combine related fields where possible
□ Use smart defaults instead of asking
□ Progressive profiling for additional data
□ Optional fields clearly marked
### User Experience
□ Single column layout
□ Clear labels above fields
□ Placeholder text as hints, not labels
□ Inline validation with clear messages
□ Tab order is logical
□ Mobile-optimized (16px font minimum)
### Validation
□ Real-time validation on blur
□ Clear, specific error messages
□ Errors appear next to fields
□ Success states confirm completion
□ Form doesn't reset on error
### Trust & Transparency
□ Privacy policy linked
□ Security indicators visible
□ Clear data usage explanation
□ No surprise fields during process
## CTA Optimization Checklist
### Copy
□ Action-oriented verb
□ First person where appropriate
□ Specific benefit mentioned
□ Risk reducers nearby
□ Avoid generic terms
### Design
□ High contrast with background
□ Sufficient size (minimum 44px height)
□ Clear hover/active states
□ Loading states for actions
□ Icon supports text meaning
### Placement
□ Above fold primary CTA
□ Repeated at logical scroll points
□ Final CTA before footer
□ Sticky mobile CTA considered
□ Multiple CTAs differentiated
### Testing
□ Button copy A/B tested
□ Color/contrast tested
□ Placement tested
□ Size tested on mobile
□ Micro-copy testedNext: Measure your optimization efforts with comprehensive CRO Analytics tracking.