Created
January 6, 2026 23:28
-
-
Save ielijose/44ab518e39fd1c71c7e2e27b1dc753d4 to your computer and use it in GitHub Desktop.
React Hook Form - Browser Autofill Detection using CSS Animations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // React Hook Form - Browser Autofill Detection | |
| // Uses CSS animations to detect when browser autofills inputs | |
| "use client"; | |
| import { useEffect, useRef, forwardRef, InputHTMLAttributes } from "react"; | |
| import { useForm, UseFormRegisterReturn, UseFormSetValue, Path } from "react-hook-form"; | |
| // ============================================================================= | |
| // CSS - Add this to your global CSS or inject via style tag | |
| // ============================================================================= | |
| const autofillCSS = ` | |
| @keyframes onAutoFillStart { from {} to {} } | |
| @keyframes onAutoFillCancel { from {} to {} } | |
| input:-webkit-autofill { | |
| animation-name: onAutoFillStart; | |
| } | |
| input:not(:-webkit-autofill) { | |
| animation-name: onAutoFillCancel; | |
| } | |
| `; | |
| // ============================================================================= | |
| // Option 1: Hook-based approach | |
| // ============================================================================= | |
| function useAutofillDetection<T extends Record<string, unknown>>( | |
| name: Path<T>, | |
| setValue: UseFormSetValue<T> | |
| ) { | |
| const ref = useRef<HTMLInputElement>(null); | |
| useEffect(() => { | |
| const input = ref.current; | |
| if (!input) return; | |
| const handleAnimationStart = (e: AnimationEvent) => { | |
| if (e.animationName === "onAutoFillStart") { | |
| requestAnimationFrame(() => { | |
| setValue(name, input.value as T[typeof name], { | |
| shouldValidate: true, | |
| shouldDirty: true, | |
| }); | |
| }); | |
| } | |
| }; | |
| input.addEventListener("animationstart", handleAnimationStart); | |
| return () => input.removeEventListener("animationstart", handleAnimationStart); | |
| }, [name, setValue]); | |
| return ref; | |
| } | |
| // Helper to merge refs | |
| const mergeRefs = <T,>( | |
| ...refs: (React.Ref<T> | undefined)[] | |
| ): React.RefCallback<T> => { | |
| return (element) => { | |
| refs.forEach((ref) => { | |
| if (typeof ref === "function") { | |
| ref(element); | |
| } else if (ref && "current" in ref) { | |
| (ref as React.MutableRefObject<T | null>).current = element; | |
| } | |
| }); | |
| }; | |
| }; | |
| // Usage with hook | |
| function LoginFormWithHook() { | |
| const { register, handleSubmit, setValue, formState: { errors } } = useForm<{ | |
| email: string; | |
| password: string; | |
| }>(); | |
| const emailRef = useAutofillDetection("email", setValue); | |
| const passwordRef = useAutofillDetection("password", setValue); | |
| return ( | |
| <> | |
| <style dangerouslySetInnerHTML={{ __html: autofillCSS }} /> | |
| <form onSubmit={handleSubmit(console.log)}> | |
| <input | |
| type="email" | |
| autoComplete="email" | |
| {...register("email", { required: "Email is required" })} | |
| ref={mergeRefs(emailRef, register("email").ref)} | |
| /> | |
| <input | |
| type="password" | |
| autoComplete="current-password" | |
| {...register("password", { required: "Password is required" })} | |
| ref={mergeRefs(passwordRef, register("password").ref)} | |
| /> | |
| <button type="submit">Login</button> | |
| </form> | |
| </> | |
| ); | |
| } | |
| // ============================================================================= | |
| // Option 2: Component-based approach (cleaner) | |
| // ============================================================================= | |
| interface AutofillInputProps extends InputHTMLAttributes<HTMLInputElement> { | |
| registration: UseFormRegisterReturn; | |
| onAutofill?: (value: string) => void; | |
| } | |
| const AutofillInput = forwardRef<HTMLInputElement, AutofillInputProps>( | |
| ({ registration, onAutofill, ...props }, _ref) => { | |
| const internalRef = useRef<HTMLInputElement>(null); | |
| useEffect(() => { | |
| const input = internalRef.current; | |
| if (!input || !onAutofill) return; | |
| const handleAnimationStart = (e: AnimationEvent) => { | |
| if (e.animationName === "onAutoFillStart") { | |
| requestAnimationFrame(() => { | |
| onAutofill(input.value); | |
| }); | |
| } | |
| }; | |
| input.addEventListener("animationstart", handleAnimationStart); | |
| return () => input.removeEventListener("animationstart", handleAnimationStart); | |
| }, [onAutofill]); | |
| return ( | |
| <input | |
| {...props} | |
| {...registration} | |
| ref={(element) => { | |
| internalRef.current = element; | |
| registration.ref(element); | |
| }} | |
| /> | |
| ); | |
| } | |
| ); | |
| AutofillInput.displayName = "AutofillInput"; | |
| // Usage with component | |
| function LoginFormWithComponent() { | |
| const { register, handleSubmit, setValue } = useForm<{ | |
| email: string; | |
| password: string; | |
| }>(); | |
| return ( | |
| <> | |
| <style dangerouslySetInnerHTML={{ __html: autofillCSS }} /> | |
| <form onSubmit={handleSubmit(console.log)}> | |
| <AutofillInput | |
| type="email" | |
| autoComplete="email" | |
| registration={register("email", { required: true })} | |
| onAutofill={(value) => setValue("email", value, { shouldValidate: true })} | |
| /> | |
| <AutofillInput | |
| type="password" | |
| autoComplete="current-password" | |
| registration={register("password", { required: true })} | |
| onAutofill={(value) => setValue("password", value, { shouldValidate: true })} | |
| /> | |
| <button type="submit">Login</button> | |
| </form> | |
| </> | |
| ); | |
| } | |
| export { useAutofillDetection, AutofillInput, mergeRefs, autofillCSS }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment