Want to build this yourself?
You can get the exact prompt I used, along with 500+ more, here:
motionsites.ai/3d-websites?p…
PROMPT:
Build a single standalone index.html (inline <style>, one inline <script type="module">, no build step, no local assets) for a 404 page called "Vulpa". Exactly one viewport tall (100vh / 100dvh, no scrolling). Shows a 3D fox loaded from a remote GLB whose head and eyes follow the cursor. Reproduce every value exactly.
=== HEAD ===
<html lang="en">, charset utf-8, viewport "width=device-width, initial-scale=1, viewport-fit=cover", <title>404 · Vulpa</title>, theme-color
#fbdc80.
Fonts (preconnect
fonts.googleapis.com +
fonts.gstatic.com crossorigin):
fonts.googleapis.com/css2?fa…
Import map: "three": "
cdn.jsdelivr.net/npm/three@0…" "three/addons/": "
cdn.jsdelivr.net/npm/three@0…"
=== CSS TOKENS (:root) === --sky-top:
#fbd873; --sky-mid:
#fce595; --sky-low:
#fef1c9; --mist:
#fffaf0; --ink:#121212; --ink-soft:
#6b6862; --fox:
#f28b3a; --fox-deep:
#e9741f; --pad:clamp(18px,4.2vw,54px); ,::before,*::after{box-sizing:border-box}. html,body{height:100%;margin:0}. body{font-family:"Manrope",system-ui,sans-serif;color:var(--ink);background:var(--sky-top);overflow:hidden;-webkit-font-smoothing:antialiased}.
=== MARKUP (order) ===
<div class="page"> <div class="sky" aria-hidden="true"> five <span class="cloud cN"> in order c5, c1, c3, c2, c4, then <span class="mist"> </div> <header class="top"> <a class="brand" href="/" aria-label="Vulpa home"> logo SVG + <span>Vulpa</span> </a> <a class="login" href="/login">Log on</a> </header> <main class="stage"> <p class="digits" aria-hidden="true">404</p> <canvas id="fox" role="img" aria-label="A little anxious fox with a map, following your pointer"></canvas> <span class="shadow" aria-hidden="true"></span> <span class="loading" id="loading">Calling the fox…</span> </main> <section class="copy"> <h1>Uh-oh! This map is wrong</h1> <p>We’ll guide you back to a place you know</p> <a class="home" href="/"> chevron SVG + "Take me home" </a> </section> </div>
Logo SVG: viewBox "0 0 40 40", fill currentColor: <circle cx="20" cy="8.4" r="5.4"/> <path d="M4.6 14.2c2.3-.9 4.6.1 5.7 2.4 1.7 3.6 5.3 6 9.7 6s8-2.4 9.7-6c1.1-2.3 3.4-3.3 5.7-2.4 2.4.9 3.4 3.5 2.4 5.9C34.7 28.1 27.9 33 20 33S5.3 28.1 2.2 20.1c-1-2.4 0-5 2.4-5.9Z"/> Chevron SVG: viewBox "0 0 24 24", fill none, stroke currentColor, stroke-width 2.4, round caps/joins, <path d="M15 5l-7 7 7 7"/>.
=== LAYOUT CSS === .page: position relative; height 100vh then 100dvh; flex column; overflow hidden; isolation isolate; background: radial-gradient(120% 70% at 50% 0%,
#fbd568 0%, rgba(251,213,104,0) 60%), linear-gradient(180deg, var(--sky-top) 0%, var(--sky-mid) 38%, var(--sky-low) 66%, var(--mist) 90%).
.sky: absolute; inset 0; z-index -1; pointer-events none.
.cloud: absolute; border-radius 50%; filter blur(14px); animation drift 38s ease-in-out infinite alternate; backgrounds: radial-gradient(closest-side, rgba(255,255,255,.95), rgba(255,255,255,0)) 0% 70% / 46% 62% no-repeat, radial-gradient(closest-side, rgba(255,255,255,.9), rgba(255,255,255,0)) 34% 20% / 52% 78% no-repeat, radial-gradient(closest-side, rgba(255,255,255,.95), rgba(255,255,255,0)) 78% 55% / 50% 70% no-repeat, radial-gradient(closest-side, rgba(255,255,255,.8), rgba(255,255,255,0)) 55% 100% / 90% 60% no-repeat .c1 {left:-9%; top:33%; width:30%; height:30%; opacity:.75} .c2 {left:4%; top:49%; width:34%; height:34%; opacity:.9; animation-duration:46s} .c3 {right:-8%; top:22%; width:30%; height:34%; opacity:.85; animation-duration:52s; animation-direction:alternate-reverse} .c4 {right:2%; top:46%; width:36%; height:36%; opacity:.9; animation-duration:42s; animation-direction:alternate-reverse} .c5 {left:8%; top:27%; width:14%; height:12%; opacity:.45}
@keyframes drift { from {transform:translateX(-1.5%)} to {transform:translateX(1.5%)} }
.mist: absolute; left -10%; right -10%; bottom -6%; height 56%; background: radial-gradient(60% 70% at 18% 62%, rgba(255,255,255,.95), rgba(255,255,255,0) 70%), radial-gradient(60% 70% at 82% 60%, rgba(255,255,255,.95), rgba(255,255,255,0) 70%), linear-gradient(180deg, rgba(255,250,240,0) 0%, rgba(255,250,240,.85) 45%, var(--mist) 75%)
.top: absolute; inset 0 0 auto 0; z-index 3; flex; align-items center; space-between; padding calc(var(--pad) * .72 + env(safe-area-inset-top)) var(--pad) 0.
.brand: inline-flex; align-items center; gap 9px; color
#fff; no underline; font 700 clamp(24px,2.5vw,32px)/1 "Quicksand","Manrope",sans-serif; letter-spacing -.02em; border-radius 10px. svg: 1.22em square, flex none.
.login: inline-flex centred; height clamp(42px,4vw,50px); padding 0 clamp(14px,1.3vw,18px); color
#fff; font 600 clamp(15px,1.35vw,17px)/1 "Manrope"; letter-spacing -.01em; border-radius 14px; background linear-gradient(180deg, rgba(255,255,255,.5), rgba(255,255,255,.28)); border 1px solid rgba(255,255,255,.7); box-shadow 0 6px 18px rgba(203,150,32,.22), inset 0 1px 0 rgba(255,255,255,.7); backdrop-filter blur(8px) (+ -webkit-); text-shadow 0 1px 2px rgba(176,124,20,.35); transition transform .2s ease, background .2s ease. :hover → translateY(-1px), background linear-gradient(180deg, rgba(255,255,255,.62), rgba(255,255,255,.38)).
.stage: relative; flex 1 1 auto; min-height 0; container-type size.
.digits: absolute; left 50%; bottom 3.5%; transform translateX(-50%) scaleX(.86); margin 0; font 700 min(116cqh, 52cqw)/.73 "Manrope"; letter-spacing -.012em; white-space nowrap; color rgba(255,255,255,.62); mask-image linear-gradient(180deg, #000 55%, rgba(0,0,0,.35) 100%) (+ -webkit-); user-select none; pointer-events none.
#fox: absolute; inset 0; 100%×100%; display block; touch-action pan-y; opacity 0; transition opacity .7s ease.
#fox.ready → opacity 1.
.shadow: absolute; left 50%; bottom 1.2%; width min(34cqh, 40cqw); height 3.2cqh; translateX(-50%); background radial-gradient(closest-side, rgba(196,132,40,.3), rgba(196,132,40,0)); opacity 0; transition opacity .7s ease. "
#fox.ready + .shadow" → opacity 1.
.loading: absolute; left 50%; top 52%; translate(-50%,-50%); font 600 14px/1 "Manrope"; color rgba(140,96,18,.75); letter-spacing .02em; transition opacity .4s ease. .hide → opacity 0.
.copy: relative; z-index 2; flex none; text-align center; padding clamp(6px,1.4vh,16px) var(--pad) calc(clamp(14px,2.3vh,24px) + env(safe-area-inset-bottom)). h1: margin 0; font 600 clamp(29px, min(4.15vw, 7.4vh), 56px)/1.08 "Manrope"; letter-spacing -.045em; text-wrap balance. .copy p: margin clamp(6px,1.3vh,12px) 0 0; font 500 clamp(16px, min(1.95vw, 3.5vh), 26px)/1.25 "Manrope"; letter-spacing -.03em; color var(--ink-soft); text-wrap balance.
.home: inline-flex; align-items center; gap 8px; margin-top clamp(12px,2.4vh,20px); height clamp(44px,6.6vh,50px); padding 0 16px 0 12px; color
#fff; font 600 clamp(15px,1.35vw,17px)/1 "Manrope"; letter-spacing -.01em; border-radius 13px; background linear-gradient(180deg,
#f7a258 0%, var(--fox) 45%, var(--fox-deep) 100%); box-shadow 0 10px 22px rgba(233,116,31,.34), 0 2px 4px rgba(190,86,12,.28), inset 0 1px 0 rgba(255,255,255,.45); text-shadow 0 1px 1px rgba(160,70,6,.4); transition transform .2s ease, box-shadow .2s ease. svg 18×18, transition transform .2s ease. :hover → translateY(-2px), first shadow 0 14px 26px rgba(233,116,31,.4), svg translateX(-3px). :active → translateY(0).
a:focus-visible: outline 3px solid
#1a1a1a; outline-offset 3px.
=== RESPONSIVE ===
@media (max-width:640px): .cloud blur(10px); .c1 {width:60%; left:-24%}; .c2 {width:64%; left:-20%}; .c3 {width:60%; right:-24%}; .c4 {width:66%; right:-20%}; .digits {font-size:min(110cqh, 72cqw); bottom:13%}.
@media (max-height:430px): .top padding-top 12px; hide .copy p.
@media (prefers-reduced-motion: reduce): no cloud animation; no transitions on .login, .home, .home svg,
#fox, .shadow.
=== 3D FOX (three.js r160) === Imports: THREE; GLTFLoader, DRACOLoader (decoder
gstatic.com/draco/versioned/…), MeshoptDecoder from three/addons.
Model URL (load directly, never download/bundle):
d8j0ntlcm91z4.cloudfront.net… Fluffy orange cartoon fox standing upright, holding a paper map, bushy cream-tipped tail curling to its left (viewer's right), BLANK WHITE eyes — the page draws the moving eyes.
Renderer: WebGLRenderer on
#fox, alpha, antialias, pixel ratio min(devicePixelRatio, 2), outputColorSpace SRGBColorSpace. Camera: PerspectiveCamera(24, 1, 0.1, 100). Lights: HemisphereLight(0xfffaf0, 0xf6cf86, 2.1); DirectionalLight(0xffffff, 1.5) at (-2.5, 4, 5); DirectionalLight(0xffe2a8, 0.8) at (3, 2.5, -4).
Rig constants (fox space: feet y=0, ear tips y=2, facing +z): neck [0.98, 1.14]; pivot [-0.165, 1.02, 0.15]; eyeL [-0.34, 1.30, 0.474]; eyeR [0.011, 1.313, 0.496]; eyeSize [0.108, 0.112, 0.075]; eyeTilt 0.25; eyeToe 0.12; headYaw 0.42; headPitch 0.24; eyeYaw 0.5; eyePitch 0.38.
On load:
model.rotation.y = -Math.PI/2 - 0.16 (mesh faces +x); updateMatrixWorld(true).
Box3 of model. s = 2/size.y. fit = Scale(s,s,s) × Translate(-centre.x, -box.min.y, -centre.z). foxWidth = size.x × s.
Each mesh: geometry.applyMatrix4(fit × mesh.matrixWorld), computeBoundingSphere, frustumCulled false; material metalness 0, roughness 1, map anisotropy max; material.onBeforeCompile = bendHead; add new THREE.Mesh(geometry, material) to Group fox.
Add Group head to fox at pivot; create two eyes inside (left side = -1, right = +1).
resize(), add "ready" to canvas, add "hide" to
#loading, renderer.setAnimationLoop(frame). On error:
#loading text = "Our fox got lost too. Please reload."
bendHead(shader): uniform mat3 uHead (shared { value: new THREE.Matrix3() }); after "
#include <common>": float headWeight(vec3 p) { float w = smoothstep(0.980, 1.140, p.y); float tail = smoothstep(0.26, 0.36, p.x) * (1.0 - smoothstep(0.0, 0.14, p.z)) * (1.0 - smoothstep(1.18, 1.28, p.y)); float map = smoothstep(0.58, 0.66, p.z) * (1.0 - smoothstep(1.0, 1.06, p.y)); return w * (1.0 - tail) * (1.0 - map); } After "
#include <beginnormal_vertex>": float hw = headWeight(position); objectNormal = normalize(mix(objectNormal, uHead * objectNormal, hw)); After "
#include <begin_vertex>": transformed = mix(transformed, P + uHead * (transformed - P), hw); P = vec3(-0.165, 1.020, 0.150). Head rotates about the neck; body, tail and map stay still.
Eye texture: one 1024×512 canvas shared by both eyes. Fill
#fffdf8. Centre at (256,256). Draw in order: disc r118
#4a2208; disc r110 with radial gradient from (256,282) r20 to (256,256) r110, stops 0
#f2a04a, .55
#c8661c, 1
#7a3a0e; disc r66
#140a04; white ellipse at (222,216) radii 24×20 rotated -0.5 rad; disc at (292,296) r9 rgba(255,255,255,.85). CanvasTexture, SRGBColorSpace, max anisotropy.
makeEye(pos, side): Group socket at pos − pivot, rotation.y = side × eyeTilt, scale = eyeSize; inside: Mesh(SphereGeometry(1, 48, 32), MeshStandardMaterial({ map, roughness .22, metalness 0 })), userData.side = side. Add socket to head, return sphere.
Pointer: Vector2 aim, look, turn; lastMove = -1e9. On window pointermove + pointerdown (passive): project (pivot.x, eyeL.y, eyeL.z) through camera → face screen position in canvas rect; reach = max(innerWidth, innerHeight) × .42; aim = ((clientX − faceX)/reach, (clientY − faceY)/reach), each clamped −1..1; lastMove =
performance.now(). document pointerleave → lastMove = -1e9.
resize() (also via ResizeObserver on .stage): w,h = stage client size (return if 0); renderer.setSize(w, h, false); camera.aspect = w/h; tan = tan(fov/2); visible = max(2/.9, (foxWidth/.86)/aspect); dist = visible/(2·tan); camera.position = (0, visible × .465, dist); lookAt(0, visible × .465, 0); updateProjectionMatrix().
frame() (THREE.Clock): dt = min(delta, .05); t = elapsed. idle =
performance.now() − lastMove > 3500. Idle + motion allowed: aim = (sin(t·.55)·.55, sin(t·.9+1)·.22 − .05). Idle + reduced motion: aim = (0,0). look.lerp(aim, 1 − exp(−dt·11)); turn.lerp(aim, 1 − exp(−dt·4.5)). euler ('YXZ') = (turn.y·headPitch, turn.x·headYaw, −turn.x·.05); head.rotation = euler; uHead = rotation as Matrix3. ex = clamp(look.x·1.25 − turn.x·.35, −1, 1); ey = clamp(look.y·1.25 − turn.y·.35, −1, 1). Each eye: rotation.set(ey·eyePitch, ex·eyeYaw − side·eyeToe, 0, 'YXZ'). fox.rotation.y = turn.x·.1. Unless reduced motion: fox.position.y = sin(t·1.7)·.008; fox.scale.y = 1 + sin(t·1.7)·.006. Render.
=== ACCEPTANCE ===
One viewport tall, no scrollbars, no horizontal overflow at 375px.
Yellow-to-cream sky, drifting clouds, bottom mist; white logo top-left; glass "Log on" top-right; huge translucent "404" behind centred fox; headline, subline, orange "Take me home" centred at bottom.
Fox faces viewer at rest; cursor turns its head and slides amber irises toward the cursor smoothly, eyes faster than head.
Single index.html; GLB fetched from the CloudFront URL.