← All components

reCAPTCHA Widget

Adds Google reCAPTCHA widget support to Next.js applications. This is a script-based integration (no npm install needed for the widget itself). The reCAPTCHA widget provides CAPTCHA protection for forms and prevents automated abuse. Requires a site key from Google reCAPTCHA admin console.

Copy the files below, or open this folder on GitHub. Each file is stamped with a limited-use licence, generator and date. SHA-256 is of the published catalog bytes, before the stamp.

id
recaptcha
stack
Next.js
version
1.0.0
compatibility
nextjs >=16 <17 · react >=19 <20
requires
nextjs-base
env vars
NEXT_PUBLIC_RECAPTCHA_SITE_KEY
Verified passing · daily buildIntegrity sha256:700cbe52437b204e

Files (2)

components/recaptcha-widget.tsx · sha256:700cbe52437b204e…
// Licensed by BotKelp — https://www.botkelp.com

'use client';

import { useEffect, useRef, useState } from 'react';

/**
 * reCAPTCHA Widget component.
 * 
 * This component renders a reCAPTCHA widget for forms.
 * Supports both v2 (checkbox) and v3 (invisible) versions.
 * 
 * Usage:
 * import { ReCaptchaWidget } from '@/components/recaptcha-widget';
 * 
 * function MyForm() {
 *   const [token, setToken] = useState('');
 *   
 *   return (
 *     <form>
 *       <ReCaptchaWidget
 *         siteKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}
 *         onVerify={(token) => setToken(token)}
 *         onExpire={() => setToken('')}
 *       />
 *       <input type="hidden" name="recaptchaToken" value={token} />
 *     </form>
 *   );
 * }
 * 
 * See: https://developers.google.com/recaptcha/docs/display
 */
export function ReCaptchaWidget({
  siteKey,
  version = 'v2',
  action = '',
  onVerify,
  onExpire,
  onError,
  size = 'normal',
  tabindex = 0,
  className = '',
}: {
  siteKey?: string;
  version?: 'v2' | 'v3' | 'v2_invisible';
  action?: string;
  onVerify?: (token: string) => void;
  onExpire?: () => void;
  onError?: (error: Error) => void;
  size?: 'normal' | 'compact';
  tabindex?: number;
  className?: string;
}) {
  const resolvedSiteKey = siteKey || process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
  const widgetRef = useRef<HTMLDivElement>(null);
  const [widgetId, setWidgetId] = useState<number | null>(null);
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    if (!resolvedSiteKey) {
      console.warn('reCAPTCHA site key is not configured');
      return;
    }

    // Check if grecaptcha is loaded
    // @ts-ignore - grecaptcha is a global object
    if (typeof window.grecaptcha === 'undefined') {
      console.warn('reCAPTCHA script is not loaded');
      return;
    }

    setIsLoaded(true);

    if (version === 'v3') {
      // For v3, we use the token directly
      // @ts-ignore
      const token = window.grecaptcha.getResponse();
      if (token && onVerify) {
        onVerify(token);
      }
    } else {
      // For v2, render the widget
      // @ts-ignore
      const id = window.grecaptcha.render(widgetRef.current, {
        sitekey: resolvedSiteKey,
        size: size,
        tabindex: tabindex,
        callback: (token: string) => {
          if (onVerify) onVerify(token);
        },
        'expired-callback': () => {
          if (onExpire) onExpire();
        },
        'error-callback': (error: Error) => {
          if (onError) onError(error);
        },
      });

      setWidgetId(id);

      return () => {
        if (widgetId !== null) {
          // @ts-ignore
          window.grecaptcha.reset(widgetId);
        }
      };
    }
  }, [resolvedSiteKey, version, onVerify, onExpire, onError, size, tabindex]);

  // Execute reCAPTCHA v3
  const executeRecaptcha = async () => {
    if (!isLoaded || !resolvedSiteKey) {
      console.warn('reCAPTCHA is not ready');
      return;
    }

    try {
      // @ts-ignore
      const token = await window.grecaptcha.execute(resolvedSiteKey, { action });
      if (onVerify) onVerify(token);
      return token;
    } catch (error) {
      if (onError) onError(error as Error);
      console.error('reCAPTCHA execution failed:', error);
      return null;
    }
  };

  // Reset reCAPTCHA
  const resetRecaptcha = () => {
    if (widgetId !== null) {
      // @ts-ignore
      window.grecaptcha.reset(widgetId);
    }
    if (onExpire) onExpire();
  };

  // Expose execute and reset methods
  // @ts-ignore - Adding custom methods to ref
  if (widgetRef.current) {
    // @ts-ignore
    widgetRef.current.execute = executeRecaptcha;
    // @ts-ignore
    widgetRef.current.reset = resetRecaptcha;
  }

  if (version === 'v3' || version === 'v2_invisible') {
    return null; // Invisible reCAPTCHA
  }

  return (
    <div
      ref={widgetRef}
      className={`g-recaptcha ${className}`}
      data-sitekey={resolvedSiteKey}
      data-size={size}
      data-tabindex={tabindex}
    />
  );
}

export default ReCaptchaWidget;