// react-spline-wrapper.tsx // Production-ready Spline wrapper for React / Next.js // Features: lazy loading, mobile detection, GPU check, fallback, fade-in on load // // Usage: // import { lazy, Suspense, useState, useEffect, useRef } from 'react'; const Spline = lazy(() => import('@splinetool/react-spline')); interface SplineBackgroundProps { sceneUrl: string; fallbackColor?: string; fallbackImageUrl?: string; mobileBreakpoint?: number; className?: string; children?: React.ReactNode; } function shouldLoadSpline(mobileBreakpoint: number): boolean { if (typeof window === 'undefined') return false; // SSR guard const isMobile = window.innerWidth < mobileBreakpoint; const isLowEnd = navigator.hardwareConcurrency <= 2; // Check WebGL support const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); const noWebGL = !gl; return !isMobile && !isLowEnd && !noWebGL; } export default function SplineBackground({ sceneUrl, fallbackColor = '#0a0a0a', fallbackImageUrl, mobileBreakpoint = 768, className = '', children, }: SplineBackgroundProps) { const [splineLoaded, setSplineLoaded] = useState(false); const [splineFailed, setSplineFailed] = useState(false); const [canLoad, setCanLoad] = useState(false); const timeoutRef = useRef>(); useEffect(() => { setCanLoad(shouldLoadSpline(mobileBreakpoint)); }, [mobileBreakpoint]); useEffect(() => { if (!canLoad) return; // If Spline hasn't loaded after 8 seconds, show fallback timeoutRef.current = setTimeout(() => { if (!splineLoaded) { setSplineFailed(true); } }, 8000); return () => clearTimeout(timeoutRef.current); }, [canLoad, splineLoaded]); function onLoad() { clearTimeout(timeoutRef.current); setSplineLoaded(true); } const showFallback = !canLoad || splineFailed; return (
{/* Fallback layer — always rendered underneath */}
{/* Spline scene — only on capable devices */} {canLoad && !splineFailed && ( )} {/* Content sits on top of everything */} {children && (
{children}
)}
); }