Smart Forms with AI: Auto-completion and Validation

Forms are the gatekeepers of web applications. Whether users are signing up, checking out, or submitting support requests, forms stand between intent and action. Yet according to Baymard Institute research, the average cart abandonment rate hovers around 70%, with complex forms being a primary culprit. AI-powered smart forms can improve completion rates by 30% or more through intelligent auto-completion, predictive validation, and conversational interfaces.

In this comprehensive guide, we will explore how to build intelligent form experiences that anticipate user needs, reduce friction, and maintain accessibility. From predictive text input and address auto-completion with the Google Places API to conversational forms and dynamic field generation, you will learn practical techniques that transform tedious data entry into seamless interactions.

Predictive Text Input with AI

Predictive text reduces cognitive load by suggesting completions as users type. While browser autocomplete handles basic scenarios, AI-powered prediction can understand context, learn from user behavior, and suggest semantically relevant options. According to UX research, effective autocomplete can reduce input time by up to 50%.

Building a Smart Autocomplete Component

Here is a complete React implementation of an AI-powered autocomplete component with debouncing, keyboard navigation, and accessibility support:

// smart-autocomplete.tsx
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';

interface AutocompleteProps {
    id: string;
    label: string;
    placeholder?: string;
    fetchSuggestions: (query: string) => Promise<Suggestion[]>;
    onSelect: (value: Suggestion) => void;
    minChars?: number;
    debounceMs?: number;
    maxSuggestions?: number;
}

interface Suggestion {
    id: string;
    value: string;
    label: string;
    metadata?: Record<string, any>;
}

export function SmartAutocomplete({
    id,
    label,
    placeholder,
    fetchSuggestions,
    onSelect,
    minChars = 2,
    debounceMs = 300,
    maxSuggestions = 8
}: AutocompleteProps) {
    const [query, setQuery] = useState('');
    const [isOpen, setIsOpen] = useState(false);
    const [highlightedIndex, setHighlightedIndex] = useState(-1);
    const inputRef = useRef<HTMLInputElement>(null);
    const listRef = useRef<HTMLUListElement>(null);
    const debounceRef = useRef<NodeJS.Timeout>();

    // Debounced query for API calls
    const [debouncedQuery, setDebouncedQuery] = useState('');

    useEffect(() => {
        if (debounceRef.current) {
            clearTimeout(debounceRef.current);
        }
        debounceRef.current = setTimeout(() => {
            setDebouncedQuery(query);
        }, debounceMs);

        return () => {
            if (debounceRef.current) {
                clearTimeout(debounceRef.current);
            }
        };
    }, [query, debounceMs]);

    // Fetch suggestions with React Query
    const { data: suggestions = [], isLoading } = useQuery({
        queryKey: ['autocomplete', debouncedQuery],
        queryFn: () => fetchSuggestions(debouncedQuery),
        enabled: debouncedQuery.length >= minChars,
        staleTime: 60000, // Cache for 1 minute
    });

    const displaySuggestions = suggestions.slice(0, maxSuggestions);

    // Keyboard navigation
    const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
        if (!isOpen || displaySuggestions.length === 0) {
            if (e.key === 'ArrowDown' && displaySuggestions.length > 0) {
                setIsOpen(true);
                setHighlightedIndex(0);
                e.preventDefault();
            }
            return;
        }

        switch (e.key) {
            case 'ArrowDown':
                e.preventDefault();
                setHighlightedIndex(prev =>
                    prev < displaySuggestions.length - 1 ? prev + 1 : 0
                );
                break;
            case 'ArrowUp':
                e.preventDefault();
                setHighlightedIndex(prev =>
                    prev > 0 ? prev - 1 : displaySuggestions.length - 1
                );
                break;
            case 'Enter':
                e.preventDefault();
                if (highlightedIndex >= 0) {
                    handleSelect(displaySuggestions[highlightedIndex]);
                }
                break;
            case 'Escape':
                setIsOpen(false);
                setHighlightedIndex(-1);
                break;
            case 'Tab':
                setIsOpen(false);
                break;
        }
    }, [isOpen, displaySuggestions, highlightedIndex]);

    const handleSelect = (suggestion: Suggestion) => {
        setQuery(suggestion.label);
        setIsOpen(false);
        setHighlightedIndex(-1);
        onSelect(suggestion);
        inputRef.current?.blur();
    };

    // Scroll highlighted item into view
    useEffect(() => {
        if (highlightedIndex >= 0 && listRef.current) {
            const highlightedElement = listRef.current.children[highlightedIndex] as HTMLElement;
            highlightedElement?.scrollIntoView({ block: 'nearest' });
        }
    }, [highlightedIndex]);

    // Close on outside click
    useEffect(() => {
        const handleClickOutside = (e: MouseEvent) => {
            if (!inputRef.current?.parentElement?.contains(e.target as Node)) {
                setIsOpen(false);
            }
        };
        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    }, []);

    const listboxId = `${id}-listbox`;

    return (
        <div className="autocomplete-container" role="combobox" aria-expanded={isOpen}>
            <label htmlFor={id} className="autocomplete-label">
                {label}
            </label>
            <div className="autocomplete-input-wrapper">
                <input
                    ref={inputRef}
                    id={id}
                    type="text"
                    value={query}
                    onChange={(e) => {
                        setQuery(e.target.value);
                        setIsOpen(true);
                        setHighlightedIndex(-1);
                    }}
                    onFocus={() => query.length >= minChars && setIsOpen(true)}
                    onKeyDown={handleKeyDown}
                    placeholder={placeholder}
                    autoComplete="off"
                    aria-autocomplete="list"
                    aria-controls={listboxId}
                    aria-activedescendant={
                        highlightedIndex >= 0 ? `${id}-option-${highlightedIndex}` : undefined
                    }
                    className="autocomplete-input"
                />
                {isLoading && (
                    <span className="autocomplete-spinner" aria-hidden="true">
                        <svg viewBox="0 0 24 24" className="spinner-icon">
                            <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
                        </svg>
                    </span>
                )}
            </div>

            {isOpen && displaySuggestions.length > 0 && (
                <ul
                    ref={listRef}
                    id={listboxId}
                    role="listbox"
                    aria-label={`${label} suggestions`}
                    className="autocomplete-list"
                >
                    {displaySuggestions.map((suggestion, index) => (
                        <li
                            key={suggestion.id}
                            id={`${id}-option-${index}`}
                            role="option"
                            aria-selected={highlightedIndex === index}
                            className={`autocomplete-option ${
                                highlightedIndex === index ? 'highlighted' : ''
                            }`}
                            onClick={() => handleSelect(suggestion)}
                            onMouseEnter={() => setHighlightedIndex(index)}
                        >
                            <HighlightedText text={suggestion.label} query={query} />
                        </li>
                    ))}
                </ul>
            )}

            {isOpen && query.length >= minChars && displaySuggestions.length === 0 && !isLoading && (
                <div className="autocomplete-no-results" role="status">
                    No suggestions found
                </div>
            )}
        </div>
    );
}

// Highlight matching text
function HighlightedText({ text, query }: { text: string; query: string }) {
    if (!query) return <>{text}</>;

    const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
    const parts = text.split(regex);

    return (
        <>
            {parts.map((part, i) =>
                regex.test(part) ? (
                    <mark key={i} className="autocomplete-highlight">{part}</mark>
                ) : (
                    <span key={i}>{part}</span>
                )
            )}
        </>
    );
}

AI-Powered Suggestion Engine

To make suggestions truly intelligent, we need a backend that combines multiple data sources and ML models:

// suggestion-engine.ts
import { OpenAI } from 'openai';

interface SuggestionContext {
    fieldType: 'name' | 'email' | 'company' | 'job_title' | 'product' | 'custom';
    userHistory: string[];
    popularChoices: string[];
    domain?: string;
}

class AISuggestionEngine {
    private openai: OpenAI;
    private cache: Map<string, { suggestions: string[]; timestamp: number }> = new Map();
    private cacheTimeout = 300000; // 5 minutes

    constructor() {
        this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
    }

    async getSuggestions(
        query: string,
        context: SuggestionContext,
        limit: number = 8
    ): Promise<string[]> {
        const cacheKey = `${context.fieldType}:${query.toLowerCase()}`;
        const cached = this.cache.get(cacheKey);

        if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
            return cached.suggestions;
        }

        // Combine multiple suggestion sources
        const [historySuggestions, popularSuggestions, aiSuggestions] = await Promise.all([
            this.getHistorySuggestions(query, context.userHistory),
            this.getPopularSuggestions(query, context.popularChoices),
            this.getAISuggestions(query, context)
        ]);

        // Rank and deduplicate suggestions
        const ranked = this.rankSuggestions(
            [...historySuggestions, ...popularSuggestions, ...aiSuggestions],
            query
        );

        const unique = [...new Set(ranked)].slice(0, limit);

        this.cache.set(cacheKey, { suggestions: unique, timestamp: Date.now() });

        return unique;
    }

    private getHistorySuggestions(query: string, history: string[]): string[] {
        const lowerQuery = query.toLowerCase();
        return history
            .filter(item => item.toLowerCase().includes(lowerQuery))
            .sort((a, b) => {
                // Prioritize prefix matches
                const aStarts = a.toLowerCase().startsWith(lowerQuery);
                const bStarts = b.toLowerCase().startsWith(lowerQuery);
                if (aStarts && !bStarts) return -1;
                if (!aStarts && bStarts) return 1;
                return a.length - b.length;
            });
    }

    private getPopularSuggestions(query: string, popular: string[]): string[] {
        const lowerQuery = query.toLowerCase();
        return popular.filter(item =>
            item.toLowerCase().includes(lowerQuery)
        );
    }

    private async getAISuggestions(
        query: string,
        context: SuggestionContext
    ): Promise<string[]> {
        if (query.length < 2) return [];

        try {
            const prompt = this.buildPrompt(query, context);

            const response = await this.openai.chat.completions.create({
                model: 'gpt-4o-mini',
                messages: [{ role: 'user', content: prompt }],
                max_tokens: 200,
                temperature: 0.3
            });

            const content = response.choices[0]?.message?.content || '';
            return this.parseSuggestions(content);
        } catch (error) {
            console.error('AI suggestion error:', error);
            return [];
        }
    }

    private buildPrompt(query: string, context: SuggestionContext): string {
        const typePrompts: Record<string, string> = {
            name: `Suggest 5 common names that start with or contain "${query}"`,
            company: `Suggest 5 well-known company names that start with or contain "${query}"`,
            job_title: `Suggest 5 professional job titles that start with or contain "${query}"`,
            product: `Suggest 5 product names that match "${query}"`,
            custom: `Suggest 5 completions for the partial text "${query}"`
        };

        return `${typePrompts[context.fieldType] || typePrompts.custom}.
Return only the suggestions as a JSON array of strings, nothing else.
${context.domain ? `Context: ${context.domain} industry` : ''}`;
    }

    private parseSuggestions(content: string): string[] {
        try {
            const jsonMatch = content.match(/\[[\s\S]*\]/);
            if (jsonMatch) {
                return JSON.parse(jsonMatch[0]);
            }
        } catch {
            // Fallback: split by newlines
            return content
                .split('\n')
                .map(line => line.replace(/^[\d\.\-\*]\s*/, '').trim())
                .filter(line => line.length > 0);
        }
        return [];
    }

    private rankSuggestions(suggestions: string[], query: string): string[] {
        const lowerQuery = query.toLowerCase();

        return suggestions.sort((a, b) => {
            const aLower = a.toLowerCase();
            const bLower = b.toLowerCase();

            // Exact match first
            if (aLower === lowerQuery) return -1;
            if (bLower === lowerQuery) return 1;

            // Prefix match second
            const aStarts = aLower.startsWith(lowerQuery);
            const bStarts = bLower.startsWith(lowerQuery);
            if (aStarts && !bStarts) return -1;
            if (!aStarts && bStarts) return 1;

            // Word boundary match third
            const aWordMatch = aLower.includes(` ${lowerQuery}`);
            const bWordMatch = bLower.includes(` ${lowerQuery}`);
            if (aWordMatch && !bWordMatch) return -1;
            if (!aWordMatch && bWordMatch) return 1;

            // Shorter strings preferred
            return a.length - b.length;
        });
    }
}

Address Auto-completion with Google Places API

Address forms are notorious for abandonment. Users struggle with format requirements, make typos, and face validation errors. The Google Places Autocomplete API solves this by predicting addresses as users type and returning structured, validated address components.

Implementing Google Places Address Input

// address-autocomplete.tsx
import React, { useEffect, useRef, useState, useCallback } from 'react';

interface AddressComponents {
    streetNumber: string;
    route: string;
    locality: string;
    administrativeArea: string;
    country: string;
    postalCode: string;
    formattedAddress: string;
    latitude: number;
    longitude: number;
}

interface AddressInputProps {
    id: string;
    label: string;
    onAddressSelect: (address: AddressComponents) => void;
    countries?: string[]; // Restrict to specific countries
    types?: ('address' | 'establishment' | 'geocode')[];
    placeholder?: string;
}

declare global {
    interface Window {
        google: typeof google;
        initGooglePlaces: () => void;
    }
}

export function AddressAutocomplete({
    id,
    label,
    onAddressSelect,
    countries = ['us', 'ca', 'gb'],
    types = ['address'],
    placeholder = 'Start typing your address...'
}: AddressInputProps) {
    const inputRef = useRef<HTMLInputElement>(null);
    const autocompleteRef = useRef<google.maps.places.Autocomplete | null>(null);
    const [inputValue, setInputValue] = useState('');
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);

    // Load Google Places script
    useEffect(() => {
        if (window.google?.maps?.places) {
            setIsLoading(false);
            return;
        }

        const script = document.createElement('script');
        script.src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_PLACES_API_KEY}&libraries=places&callback=initGooglePlaces`;
        script.async = true;
        script.defer = true;

        window.initGooglePlaces = () => {
            setIsLoading(false);
        };

        script.onerror = () => {
            setError('Failed to load address autocomplete');
            setIsLoading(false);
        };

        document.head.appendChild(script);

        return () => {
            delete window.initGooglePlaces;
        };
    }, []);

    // Initialize autocomplete
    useEffect(() => {
        if (isLoading || !inputRef.current || !window.google?.maps?.places) {
            return;
        }

        const options: google.maps.places.AutocompleteOptions = {
            types,
            componentRestrictions: countries.length > 0 ? { country: countries } : undefined,
            fields: [
                'address_components',
                'formatted_address',
                'geometry',
                'place_id'
            ]
        };

        autocompleteRef.current = new google.maps.places.Autocomplete(
            inputRef.current,
            options
        );

        const listener = autocompleteRef.current.addListener('place_changed', () => {
            const place = autocompleteRef.current?.getPlace();
            if (place?.address_components) {
                const address = parseAddressComponents(place);
                setInputValue(address.formattedAddress);
                onAddressSelect(address);
            }
        });

        return () => {
            google.maps.event.removeListener(listener);
        };
    }, [isLoading, countries, types, onAddressSelect]);

    const parseAddressComponents = (
        place: google.maps.places.PlaceResult
    ): AddressComponents => {
        const components = place.address_components || [];
        const getComponent = (type: string): string => {
            const component = components.find(c => c.types.includes(type));
            return component?.long_name || '';
        };

        return {
            streetNumber: getComponent('street_number'),
            route: getComponent('route'),
            locality: getComponent('locality') || getComponent('sublocality'),
            administrativeArea: getComponent('administrative_area_level_1'),
            country: getComponent('country'),
            postalCode: getComponent('postal_code'),
            formattedAddress: place.formatted_address || '',
            latitude: place.geometry?.location?.lat() || 0,
            longitude: place.geometry?.location?.lng() || 0
        };
    };

    // Handle manual input for accessibility
    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setInputValue(e.target.value);
    };

    // Keyboard accessibility for dropdown
    const handleKeyDown = (e: React.KeyboardEvent) => {
        // Google Places handles most keyboard navigation
        // but we ensure Escape clears the input
        if (e.key === 'Escape') {
            setInputValue('');
            inputRef.current?.blur();
        }
    };

    if (error) {
        return (
            <div className="address-input-error">
                <label htmlFor={id}>{label}</label>
                <input
                    id={id}
                    type="text"
                    placeholder="Enter address manually"
                    onChange={(e) => setInputValue(e.target.value)}
                    className="address-input"
                />
                <span className="error-text">{error}</span>
            </div>
        );
    }

    return (
        <div className="address-input-container">
            <label htmlFor={id} className="address-label">
                {label}
            </label>
            <div className="address-input-wrapper">
                <input
                    ref={inputRef}
                    id={id}
                    type="text"
                    value={inputValue}
                    onChange={handleInputChange}
                    onKeyDown={handleKeyDown}
                    placeholder={isLoading ? 'Loading...' : placeholder}
                    disabled={isLoading}
                    autoComplete="off"
                    aria-describedby={`${id}-hint`}
                    className="address-input"
                />
                {isLoading && <span className="loading-indicator" aria-hidden="true" />}
            </div>
            <span id={`${id}-hint`} className="address-hint">
                Start typing and select from suggestions
            </span>
        </div>
    );
}

// Hook for using address data in forms
export function useAddressForm() {
    const [address, setAddress] = useState<AddressComponents | null>(null);

    const handleAddressSelect = useCallback((selectedAddress: AddressComponents) => {
        setAddress(selectedAddress);
    }, []);

    const addressFields = address ? {
        street: `${address.streetNumber} ${address.route}`.trim(),
        city: address.locality,
        state: address.administrativeArea,
        postalCode: address.postalCode,
        country: address.country
    } : null;

    return { address, addressFields, handleAddressSelect };
}

Intelligent Field Validation

Traditional validation is reactive: it waits for users to submit and then displays errors. AI-powered validation is proactive, providing real-time feedback and suggestions before errors occur. This approach aligns with WCAG form validation guidelines while improving user experience.

// intelligent-validator.ts
import { z } from 'zod';

interface ValidationResult {
    isValid: boolean;
    errors: ValidationError[];
    suggestions: string[];
    confidence: number;
}

interface ValidationError {
    field: string;
    message: string;
    severity: 'error' | 'warning' | 'info';
}

class IntelligentFormValidator {
    private schemas: Map<string, z.ZodSchema> = new Map();
    private commonTypos: Map<string, string> = new Map([
        ['gmial.com', 'gmail.com'],
        ['gmal.com', 'gmail.com'],
        ['yaho.com', 'yahoo.com'],
        ['hotmal.com', 'hotmail.com'],
        ['outloo.com', 'outlook.com']
    ]);

    // Email validation with typo detection
    validateEmail(email: string): ValidationResult {
        const errors: ValidationError[] = [];
        const suggestions: string[] = [];

        // Basic format check
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(email)) {
            errors.push({
                field: 'email',
                message: 'Please enter a valid email address',
                severity: 'error'
            });
            return { isValid: false, errors, suggestions, confidence: 0 };
        }

        // Domain typo detection
        const [localPart, domain] = email.split('@');
        const correctedDomain = this.commonTypos.get(domain.toLowerCase());

        if (correctedDomain) {
            suggestions.push(`Did you mean ${localPart}@${correctedDomain}?`);
            errors.push({
                field: 'email',
                message: `"${domain}" might be a typo`,
                severity: 'warning'
            });
        }

        // Check for common patterns
        if (domain.split('.').length < 2) {
            errors.push({
                field: 'email',
                message: 'Email domain appears incomplete',
                severity: 'error'
            });
        }

        // Disposable email detection
        const disposableDomains = ['tempmail.com', 'throwaway.com', '10minutemail.com'];
        if (disposableDomains.includes(domain.toLowerCase())) {
            errors.push({
                field: 'email',
                message: 'Please use a permanent email address',
                severity: 'warning'
            });
        }

        return {
            isValid: errors.filter(e => e.severity === 'error').length === 0,
            errors,
            suggestions,
            confidence: errors.length === 0 ? 1 : 0.7
        };
    }

    // Phone number validation with formatting
    validatePhone(phone: string, country: string = 'US'): ValidationResult {
        const errors: ValidationError[] = [];
        const suggestions: string[] = [];

        // Remove non-numeric characters for analysis
        const digits = phone.replace(/\D/g, '');

        const formats: Record<string, { length: number; format: string }> = {
            US: { length: 10, format: '(XXX) XXX-XXXX' },
            UK: { length: 11, format: '+44 XXXX XXXXXX' },
            IN: { length: 10, format: '+91 XXXXX XXXXX' }
        };

        const countryFormat = formats[country] || formats.US;

        if (digits.length !== countryFormat.length) {
            errors.push({
                field: 'phone',
                message: `Phone number should be ${countryFormat.length} digits`,
                severity: 'error'
            });
            suggestions.push(`Expected format: ${countryFormat.format}`);
        }

        // Format suggestion
        if (digits.length === countryFormat.length && country === 'US') {
            const formatted = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
            if (phone !== formatted) {
                suggestions.push(`Formatted: ${formatted}`);
            }
        }

        return {
            isValid: errors.filter(e => e.severity === 'error').length === 0,
            errors,
            suggestions,
            confidence: errors.length === 0 ? 1 : 0.5
        };
    }

    // Real-time password strength analyzer
    analyzePasswordStrength(password: string): {
        score: number;
        feedback: string[];
        suggestions: string[];
    } {
        let score = 0;
        const feedback: string[] = [];
        const suggestions: string[] = [];

        // Length check
        if (password.length >= 12) {
            score += 25;
            feedback.push('Good length');
        } else if (password.length >= 8) {
            score += 15;
            suggestions.push('Consider using 12+ characters');
        } else {
            suggestions.push('Password should be at least 8 characters');
        }

        // Complexity checks
        if (/[a-z]/.test(password) && /[A-Z]/.test(password)) {
            score += 20;
            feedback.push('Mix of upper and lowercase');
        } else {
            suggestions.push('Add both upper and lowercase letters');
        }

        if (/\d/.test(password)) {
            score += 20;
            feedback.push('Contains numbers');
        } else {
            suggestions.push('Add numbers');
        }

        if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
            score += 20;
            feedback.push('Contains special characters');
        } else {
            suggestions.push('Add special characters (!@#$%^&*)');
        }

        // Common pattern detection
        const commonPatterns = [
            /^123/, /password/i, /qwerty/i, /^abc/i
        ];
        const hasCommonPattern = commonPatterns.some(p => p.test(password));
        if (hasCommonPattern) {
            score -= 20;
            suggestions.push('Avoid common patterns like "123" or "password"');
        }

        // Sequential characters
        if (/(.)\1{2,}/.test(password)) {
            score -= 10;
            suggestions.push('Avoid repeating characters');
        }

        return {
            score: Math.max(0, Math.min(100, score)),
            feedback,
            suggestions
        };
    }
}

// React hook for real-time validation
export function useFieldValidation(
    value: string,
    fieldType: 'email' | 'phone' | 'password',
    options?: { country?: string; debounceMs?: number }
) {
    const [result, setResult] = useState<ValidationResult | null>(null);
    const [isValidating, setIsValidating] = useState(false);
    const validator = useMemo(() => new IntelligentFormValidator(), []);

    useEffect(() => {
        if (!value) {
            setResult(null);
            return;
        }

        setIsValidating(true);
        const timeout = setTimeout(() => {
            let validationResult: ValidationResult;

            switch (fieldType) {
                case 'email':
                    validationResult = validator.validateEmail(value);
                    break;
                case 'phone':
                    validationResult = validator.validatePhone(value, options?.country);
                    break;
                case 'password':
                    const strength = validator.analyzePasswordStrength(value);
                    validationResult = {
                        isValid: strength.score >= 60,
                        errors: strength.score < 60 ? [{
                            field: 'password',
                            message: 'Password is not strong enough',
                            severity: 'warning'
                        }] : [],
                        suggestions: strength.suggestions,
                        confidence: strength.score / 100
                    };
                    break;
            }

            setResult(validationResult);
            setIsValidating(false);
        }, options?.debounceMs || 300);

        return () => clearTimeout(timeout);
    }, [value, fieldType, options?.country, options?.debounceMs, validator]);

    return { result, isValidating };
}

Conversational Forms

Conversational forms transform data collection from a static form into an interactive dialogue. By presenting one question at a time and understanding natural language responses, they reduce cognitive load and create a more engaging experience. Tools like Typeform and Landbot have proven that conversational interfaces can achieve 40% higher completion rates than traditional forms.

// conversational-form.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { AnimatePresence, motion } from 'framer-motion';

interface FormStep {
    id: string;
    type: 'text' | 'email' | 'phone' | 'select' | 'multiselect' | 'date' | 'number';
    question: string;
    placeholder?: string;
    options?: { value: string; label: string }[];
    validation?: (value: any) => string | null;
    conditionalShow?: (answers: Record<string, any>) => boolean;
}

interface ConversationalFormProps {
    steps: FormStep[];
    onComplete: (answers: Record<string, any>) => void;
    onStepChange?: (stepIndex: number, totalSteps: number) => void;
    welcomeMessage?: string;
    completionMessage?: string;
}

export function ConversationalForm({
    steps,
    onComplete,
    onStepChange,
    welcomeMessage = "Hi! Let's get started.",
    completionMessage = "Thank you! We've received your information."
}: ConversationalFormProps) {
    const [currentStepIndex, setCurrentStepIndex] = useState(-1); // -1 for welcome
    const [answers, setAnswers] = useState<Record<string, any>>({});
    const [inputValue, setInputValue] = useState('');
    const [error, setError] = useState<string | null>(null);
    const [isComplete, setIsComplete] = useState(false);
    const [messages, setMessages] = useState<Array<{
        type: 'bot' | 'user';
        content: string;
    }>>([]);
    const messagesEndRef = useRef<HTMLDivElement>(null);
    const inputRef = useRef<HTMLInputElement>(null);

    // Filter visible steps based on conditions
    const visibleSteps = steps.filter(step =>
        !step.conditionalShow || step.conditionalShow(answers)
    );

    const currentStep = currentStepIndex >= 0 ? visibleSteps[currentStepIndex] : null;

    // Scroll to bottom when messages change
    useEffect(() => {
        messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
    }, [messages]);

    // Focus input when step changes
    useEffect(() => {
        inputRef.current?.focus();
    }, [currentStepIndex]);

    // Notify parent of step changes
    useEffect(() => {
        if (currentStepIndex >= 0) {
            onStepChange?.(currentStepIndex + 1, visibleSteps.length);
        }
    }, [currentStepIndex, visibleSteps.length, onStepChange]);

    // Start conversation
    useEffect(() => {
        setMessages([{ type: 'bot', content: welcomeMessage }]);
        const timer = setTimeout(() => {
            setCurrentStepIndex(0);
            if (visibleSteps[0]) {
                setMessages(prev => [...prev, {
                    type: 'bot',
                    content: visibleSteps[0].question
                }]);
            }
        }, 1000);
        return () => clearTimeout(timer);
    }, []);

    const handleSubmit = useCallback((e?: React.FormEvent) => {
        e?.preventDefault();

        if (!currentStep || !inputValue.trim()) return;

        // Validate input
        if (currentStep.validation) {
            const validationError = currentStep.validation(inputValue);
            if (validationError) {
                setError(validationError);
                return;
            }
        }

        setError(null);

        // Add user message
        setMessages(prev => [...prev, { type: 'user', content: inputValue }]);

        // Store answer
        const newAnswers = { ...answers, [currentStep.id]: inputValue };
        setAnswers(newAnswers);
        setInputValue('');

        // Move to next step
        const nextIndex = currentStepIndex + 1;

        setTimeout(() => {
            if (nextIndex < visibleSteps.length) {
                const nextStep = visibleSteps[nextIndex];
                // Check if next step should be shown based on new answers
                if (!nextStep.conditionalShow || nextStep.conditionalShow(newAnswers)) {
                    setCurrentStepIndex(nextIndex);
                    setMessages(prev => [...prev, {
                        type: 'bot',
                        content: nextStep.question
                    }]);
                } else {
                    // Skip to next valid step
                    handleNextValidStep(nextIndex + 1, newAnswers);
                }
            } else {
                // Form complete
                setIsComplete(true);
                setMessages(prev => [...prev, {
                    type: 'bot',
                    content: completionMessage
                }]);
                onComplete(newAnswers);
            }
        }, 500);
    }, [currentStep, inputValue, answers, currentStepIndex, visibleSteps]);

    const handleNextValidStep = (fromIndex: number, currentAnswers: Record<string, any>) => {
        for (let i = fromIndex; i < visibleSteps.length; i++) {
            const step = visibleSteps[i];
            if (!step.conditionalShow || step.conditionalShow(currentAnswers)) {
                setCurrentStepIndex(i);
                setMessages(prev => [...prev, {
                    type: 'bot',
                    content: step.question
                }]);
                return;
            }
        }
        // No more steps
        setIsComplete(true);
        setMessages(prev => [...prev, {
            type: 'bot',
            content: completionMessage
        }]);
        onComplete(currentAnswers);
    };

    // Handle select options
    const handleOptionSelect = (option: { value: string; label: string }) => {
        setInputValue(option.value);
        // Auto-submit for select types
        setTimeout(() => {
            const fakeEvent = { preventDefault: () => {} } as React.FormEvent;
            handleSubmit(fakeEvent);
        }, 0);
    };

    return (
        <div className="conversational-form" role="form" aria-label="Conversational form">
            <div className="messages-container" aria-live="polite">
                <AnimatePresence>
                    {messages.map((message, index) => (
                        <motion.div
                            key={index}
                            initial={{ opacity: 0, y: 20 }}
                            animate={{ opacity: 1, y: 0 }}
                            exit={{ opacity: 0 }}
                            transition={{ duration: 0.3 }}
                            className={`message ${message.type}`}
                        >
                            {message.type === 'bot' && (
                                <div className="bot-avatar" aria-hidden="true">
                                    <svg viewBox="0 0 24 24" fill="currentColor">
                                        <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/>
                                    </svg>
                                </div>
                            )}
                            <div className="message-content">{message.content}</div>
                        </motion.div>
                    ))}
                </AnimatePresence>
                <div ref={messagesEndRef} />
            </div>

            {!isComplete && currentStep && (
                <div className="input-container">
                    {currentStep.type === 'select' && currentStep.options ? (
                        <div className="options-grid" role="listbox">
                            {currentStep.options.map(option => (
                                <button
                                    key={option.value}
                                    type="button"
                                    onClick={() => handleOptionSelect(option)}
                                    className="option-button"
                                    role="option"
                                >
                                    {option.label}
                                </button>
                            ))}
                        </div>
                    ) : (
                        <form onSubmit={handleSubmit} className="text-input-form">
                            <input
                                ref={inputRef}
                                type={currentStep.type === 'email' ? 'email' :
                                      currentStep.type === 'phone' ? 'tel' :
                                      currentStep.type === 'number' ? 'number' :
                                      currentStep.type === 'date' ? 'date' : 'text'}
                                value={inputValue}
                                onChange={(e) => setInputValue(e.target.value)}
                                placeholder={currentStep.placeholder || 'Type your answer...'}
                                aria-label={currentStep.question}
                                aria-invalid={!!error}
                                aria-describedby={error ? 'input-error' : undefined}
                                className="conversation-input"
                            />
                            <button
                                type="submit"
                                disabled={!inputValue.trim()}
                                className="send-button"
                                aria-label="Send"
                            >
                                <svg viewBox="0 0 24 24" fill="currentColor">
                                    <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
                                </svg>
                            </button>
                        </form>
                    )}
                    {error && (
                        <div id="input-error" className="error-message" role="alert">
                            {error}
                        </div>
                    )}
                </div>
            )}

            {/* Progress indicator */}
            {!isComplete && currentStepIndex >= 0 && (
                <div className="progress-indicator" aria-hidden="true">
                    <div
                        className="progress-bar"
                        style={{ width: `${((currentStepIndex + 1) / visibleSteps.length) * 100}%` }}
                    />
                    <span className="progress-text">
                        {currentStepIndex + 1} of {visibleSteps.length}
                    </span>
                </div>
            )}
        </div>
    );
}

Form Abandonment Prevention

Form abandonment is a critical metric for conversion optimization. AI can detect abandonment signals and intervene with targeted assistance. According to Formisimo research, 29% of users abandon forms due to length, while 27% leave due to security concerns.

// abandonment-prevention.ts
interface AbandonmentSignal {
    type: 'mouse_leave' | 'idle' | 'rapid_deletion' | 'error_frustration' | 'field_hesitation';
    timestamp: number;
    metadata?: Record<string, any>;
}

interface InterventionConfig {
    enabled: boolean;
    threshold: number;
    action: 'tooltip' | 'modal' | 'chat' | 'save_progress';
    message: string;
}

class FormAbandonmentDetector {
    private signals: AbandonmentSignal[] = [];
    private interventionTriggered = false;
    private idleTimer: NodeJS.Timeout | null = null;
    private lastActivity = Date.now();
    private errorCount = 0;
    private deletionCount = 0;
    private fieldTimes: Map<string, number> = new Map();

    constructor(
        private formElement: HTMLFormElement,
        private onAbandonmentDetected: (signals: AbandonmentSignal[]) => void,
        private config: {
            idleThreshold: number;
            deletionThreshold: number;
            errorThreshold: number;
            hesitationThreshold: number;
        } = {
            idleThreshold: 30000, // 30 seconds
            deletionThreshold: 3,
            errorThreshold: 3,
            hesitationThreshold: 10000 // 10 seconds on one field
        }
    ) {
        this.attachListeners();
    }

    private attachListeners(): void {
        // Mouse leave detection (exit intent)
        document.addEventListener('mouseleave', this.handleMouseLeave);

        // Idle detection
        this.resetIdleTimer();
        document.addEventListener('mousemove', this.resetIdleTimer);
        document.addEventListener('keypress', this.resetIdleTimer);

        // Input monitoring
        this.formElement.addEventListener('input', this.handleInput);
        this.formElement.addEventListener('focusin', this.handleFocusIn);
        this.formElement.addEventListener('focusout', this.handleFocusOut);

        // Error detection
        this.formElement.addEventListener('invalid', this.handleInvalid, true);
    }

    private handleMouseLeave = (e: MouseEvent): void => {
        // Only trigger for upward mouse movement (exit intent)
        if (e.clientY < 50 && !this.interventionTriggered) {
            this.addSignal({
                type: 'mouse_leave',
                timestamp: Date.now(),
                metadata: { clientY: e.clientY }
            });
            this.checkIntervention();
        }
    };

    private resetIdleTimer = (): void => {
        this.lastActivity = Date.now();
        if (this.idleTimer) {
            clearTimeout(this.idleTimer);
        }
        this.idleTimer = setTimeout(() => {
            this.addSignal({
                type: 'idle',
                timestamp: Date.now(),
                metadata: { idleDuration: this.config.idleThreshold }
            });
            this.checkIntervention();
        }, this.config.idleThreshold);
    };

    private handleInput = (e: Event): void => {
        const target = e.target as HTMLInputElement;
        const inputType = (e as InputEvent).inputType;

        // Detect rapid deletion (frustration signal)
        if (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward') {
            this.deletionCount++;
            if (this.deletionCount >= this.config.deletionThreshold) {
                this.addSignal({
                    type: 'rapid_deletion',
                    timestamp: Date.now(),
                    metadata: { fieldName: target.name, deletionCount: this.deletionCount }
                });
                this.deletionCount = 0;
                this.checkIntervention();
            }
        } else {
            this.deletionCount = 0;
        }
    };

    private handleFocusIn = (e: FocusEvent): void => {
        const target = e.target as HTMLInputElement;
        this.fieldTimes.set(target.name, Date.now());
    };

    private handleFocusOut = (e: FocusEvent): void => {
        const target = e.target as HTMLInputElement;
        const startTime = this.fieldTimes.get(target.name);

        if (startTime) {
            const duration = Date.now() - startTime;
            if (duration > this.config.hesitationThreshold && !target.value) {
                this.addSignal({
                    type: 'field_hesitation',
                    timestamp: Date.now(),
                    metadata: {
                        fieldName: target.name,
                        duration,
                        fieldLabel: this.getFieldLabel(target)
                    }
                });
                this.checkIntervention();
            }
        }
    };

    private handleInvalid = (e: Event): void => {
        this.errorCount++;
        if (this.errorCount >= this.config.errorThreshold) {
            this.addSignal({
                type: 'error_frustration',
                timestamp: Date.now(),
                metadata: { errorCount: this.errorCount }
            });
            this.checkIntervention();
        }
    };

    private getFieldLabel(input: HTMLInputElement): string {
        const label = this.formElement.querySelector(`label[for="${input.id}"]`);
        return label?.textContent || input.placeholder || input.name;
    }

    private addSignal(signal: AbandonmentSignal): void {
        this.signals.push(signal);
    }

    private checkIntervention(): void {
        if (this.interventionTriggered) return;

        // Calculate abandonment risk score
        const riskScore = this.calculateRiskScore();

        if (riskScore >= 0.7) {
            this.interventionTriggered = true;
            this.onAbandonmentDetected(this.signals);
        }
    }

    private calculateRiskScore(): number {
        let score = 0;
        const weights = {
            mouse_leave: 0.3,
            idle: 0.2,
            rapid_deletion: 0.25,
            error_frustration: 0.35,
            field_hesitation: 0.15
        };

        const signalTypes = new Set(this.signals.map(s => s.type));
        signalTypes.forEach(type => {
            score += weights[type] || 0;
        });

        return Math.min(score, 1);
    }

    public destroy(): void {
        document.removeEventListener('mouseleave', this.handleMouseLeave);
        document.removeEventListener('mousemove', this.resetIdleTimer);
        document.removeEventListener('keypress', this.resetIdleTimer);
        if (this.idleTimer) {
            clearTimeout(this.idleTimer);
        }
    }
}

// Save progress automatically
export function useFormPersistence(
    formId: string,
    values: Record<string, any>,
    excludeFields: string[] = ['password', 'creditCard', 'cvv']
) {
    const storageKey = `form_progress_${formId}`;

    // Save on change (debounced)
    useEffect(() => {
        const timer = setTimeout(() => {
            const safeValues = { ...values };
            excludeFields.forEach(field => delete safeValues[field]);
            localStorage.setItem(storageKey, JSON.stringify({
                values: safeValues,
                timestamp: Date.now()
            }));
        }, 1000);

        return () => clearTimeout(timer);
    }, [values, storageKey, excludeFields]);

    // Restore on mount
    const restoreProgress = useCallback(() => {
        const saved = localStorage.getItem(storageKey);
        if (saved) {
            const { values, timestamp } = JSON.parse(saved);
            // Only restore if less than 24 hours old
            if (Date.now() - timestamp < 86400000) {
                return values;
            }
        }
        return null;
    }, [storageKey]);

    const clearProgress = useCallback(() => {
        localStorage.removeItem(storageKey);
    }, [storageKey]);

    return { restoreProgress, clearProgress };
}

Maintaining Accessibility

Smart forms must remain accessible to all users, including those using screen readers, keyboard navigation, or other assistive technologies. Following WCAG 2.1 Input Assistance guidelines is essential when implementing AI-powered features. For more on accessibility implementation, see our guide on AI for Accessibility (a11y) Implementation.

// accessible-form-field.tsx
interface AccessibleFieldProps {
    id: string;
    label: string;
    type?: string;
    error?: string;
    hint?: string;
    required?: boolean;
    suggestions?: string[];
    onSuggestionSelect?: (suggestion: string) => void;
    value: string;
    onChange: (value: string) => void;
}

export function AccessibleFormField({
    id,
    label,
    type = 'text',
    error,
    hint,
    required = false,
    suggestions = [],
    onSuggestionSelect,
    value,
    onChange
}: AccessibleFieldProps) {
    const [showSuggestions, setShowSuggestions] = useState(false);
    const [highlightedIndex, setHighlightedIndex] = useState(-1);
    const errorId = `${id}-error`;
    const hintId = `${id}-hint`;
    const suggestionsId = `${id}-suggestions`;

    // Combine aria-describedby values
    const describedBy = [
        hint && hintId,
        error && errorId,
        showSuggestions && suggestions.length > 0 && suggestionsId
    ].filter(Boolean).join(' ') || undefined;

    return (
        <div className="form-field">
            <label htmlFor={id} className="form-label">
                {label}
                {required && <span className="required-indicator" aria-hidden="true">*</span>}
                {required && <span className="sr-only">(required)</span>}
            </label>

            {hint && (
                <p id={hintId} className="form-hint">
                    {hint}
                </p>
            )}

            <div className="input-wrapper">
                <input
                    id={id}
                    type={type}
                    value={value}
                    onChange={(e) => onChange(e.target.value)}
                    onFocus={() => setShowSuggestions(true)}
                    onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
                    required={required}
                    aria-required={required}
                    aria-invalid={!!error}
                    aria-describedby={describedBy}
                    aria-autocomplete={suggestions.length > 0 ? 'list' : undefined}
                    aria-controls={suggestions.length > 0 ? suggestionsId : undefined}
                    aria-activedescendant={
                        highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : undefined
                    }
                    className={`form-input ${error ? 'has-error' : ''}`}
                />

                {/* Suggestions dropdown with live region */}
                {showSuggestions && suggestions.length > 0 && (
                    <>
                        <ul
                            id={suggestionsId}
                            role="listbox"
                            aria-label={`Suggestions for ${label}`}
                            className="suggestions-list"
                        >
                            {suggestions.map((suggestion, index) => (
                                <li
                                    key={suggestion}
                                    id={`${id}-suggestion-${index}`}
                                    role="option"
                                    aria-selected={highlightedIndex === index}
                                    onClick={() => onSuggestionSelect?.(suggestion)}
                                    onMouseEnter={() => setHighlightedIndex(index)}
                                    className={highlightedIndex === index ? 'highlighted' : ''}
                                >
                                    {suggestion}
                                </li>
                            ))}
                        </ul>
                        <div aria-live="polite" className="sr-only">
                            {suggestions.length} suggestions available
                        </div>
                    </>
                )}
            </div>

            {/* Error message with live region */}
            {error && (
                <p id={errorId} className="form-error" role="alert">
                    <span aria-hidden="true">!</span>
                    {error}
                </p>
            )}
        </div>
    );
}

// Screen reader only styles (add to CSS)
/*
.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;
}
*/

Measuring Form Performance

Track the impact of AI enhancements with comprehensive metrics. The goal is to measure not just completion rates, but the quality of the user experience:

// form-analytics.ts
interface FormMetrics {
    formId: string;
    sessionId: string;
    startTime: number;
    completionTime?: number;
    abandoned: boolean;
    abandonedAt?: string;
    fieldInteractions: FieldInteraction[];
    suggestionAcceptanceRate: number;
    validationErrors: number;
    autocompletionsUsed: number;
}

interface FieldInteraction {
    fieldId: string;
    focusTime: number;
    blurTime?: number;
    changes: number;
    errorsShown: number;
    suggestionsShown: number;
    suggestionAccepted: boolean;
}

class FormAnalytics {
    private metrics: FormMetrics;
    private currentField: FieldInteraction | null = null;

    constructor(formId: string) {
        this.metrics = {
            formId,
            sessionId: crypto.randomUUID(),
            startTime: Date.now(),
            abandoned: true,
            fieldInteractions: [],
            suggestionAcceptanceRate: 0,
            validationErrors: 0,
            autocompletionsUsed: 0
        };
    }

    trackFieldFocus(fieldId: string): void {
        this.currentField = {
            fieldId,
            focusTime: Date.now(),
            changes: 0,
            errorsShown: 0,
            suggestionsShown: 0,
            suggestionAccepted: false
        };
    }

    trackFieldBlur(): void {
        if (this.currentField) {
            this.currentField.blurTime = Date.now();
            this.metrics.fieldInteractions.push({ ...this.currentField });
            this.currentField = null;
        }
    }

    trackChange(): void {
        if (this.currentField) {
            this.currentField.changes++;
        }
    }

    trackSuggestionShown(): void {
        if (this.currentField) {
            this.currentField.suggestionsShown++;
        }
    }

    trackSuggestionAccepted(): void {
        if (this.currentField) {
            this.currentField.suggestionAccepted = true;
        }
        this.metrics.autocompletionsUsed++;
    }

    trackValidationError(): void {
        this.metrics.validationErrors++;
        if (this.currentField) {
            this.currentField.errorsShown++;
        }
    }

    trackSubmission(success: boolean): void {
        this.metrics.abandoned = !success;
        this.metrics.completionTime = Date.now();
        this.calculateSuggestionAcceptanceRate();
        this.sendMetrics();
    }

    trackAbandonment(lastFieldId?: string): void {
        this.metrics.abandoned = true;
        this.metrics.abandonedAt = lastFieldId;
        this.calculateSuggestionAcceptanceRate();
        this.sendMetrics();
    }

    private calculateSuggestionAcceptanceRate(): void {
        const fieldsWithSuggestions = this.metrics.fieldInteractions.filter(
            f => f.suggestionsShown > 0
        );
        if (fieldsWithSuggestions.length > 0) {
            const accepted = fieldsWithSuggestions.filter(f => f.suggestionAccepted).length;
            this.metrics.suggestionAcceptanceRate = accepted / fieldsWithSuggestions.length;
        }
    }

    private async sendMetrics(): Promise<void> {
        const summary = {
            ...this.metrics,
            totalTime: (this.metrics.completionTime || Date.now()) - this.metrics.startTime,
            avgFieldTime: this.calculateAvgFieldTime(),
            completionRate: this.metrics.abandoned ? 0 : 1
        };

        await fetch('/api/analytics/form-metrics', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(summary)
        });
    }

    private calculateAvgFieldTime(): number {
        const times = this.metrics.fieldInteractions
            .filter(f => f.blurTime)
            .map(f => f.blurTime! - f.focusTime);
        return times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : 0;
    }
}

Key Takeaways

Remember These Points

  • Predictive text reduces friction: AI-powered autocomplete with debouncing and keyboard navigation can reduce input time by 50%
  • Address autocomplete is essential: Google Places API integration eliminates address validation errors and reduces abandonment by 25-30%
  • Proactive validation beats reactive: Real-time feedback with typo detection and suggestions prevents errors before submission
  • Conversational forms increase completion: One question at a time with progress indicators achieves 40% higher completion rates
  • Detect abandonment signals early: Exit intent, idle time, and frustration patterns trigger timely interventions
  • Accessibility is non-negotiable: ARIA attributes, keyboard navigation, and screen reader support must accompany all AI features
  • Measure everything: Track suggestion acceptance, field time, validation errors, and abandonment points to continuously optimize

Conclusion

Smart forms powered by AI represent a significant evolution in web user experience. By implementing predictive text input, address auto-completion with services like the Google Places API, intelligent validation, and conversational interfaces, you can achieve the 30%+ improvement in form completion rates that leading organizations report.

The key is to view forms not as data collection barriers but as opportunities for intelligent assistance. Every keystroke is a chance to anticipate user needs, every validation error an opportunity to guide rather than frustrate, and every moment of hesitation a signal to provide helpful context.

For related topics, explore our guides on AI Bias in Accessibility for inclusive form design and AI-Driven Personalization Engines for dynamic form customization based on user behavior. External resources like the web.dev Forms Guide and Nielsen Norman Group's form design research provide additional best practices for form optimization.