---
name: build-3d-website
description: "Build interactive 3D marketing sites with React Three Fiber and drei: 3D heroes, floating objects, tilt cards, scroll-driven scenes, WebGL showcases. Use when asked for a 3D landing page, WebGL hero, immersive explorer, or a premium wow page, or on mentions of three.js, R3F, tilt card. Not for playable games with mechanics, AI, or saves: use build-3d-game."
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: original
  category: 3D & Games
---

# Build a 3D Interactive Website

One focused, SSR-safe, performant hero scene beats a sprawling broken one. Build the hero first, guard every SSR and loading edge, and verify on a phone-sized viewport before calling it done.

## Two modes

- **Build mode (default).** Implement in the current project with R3F plus drei.
- **Prompt mode.** Only when the user explicitly asks for a prompt to paste into another builder. Emit a single self-contained master prompt covering scene, interactions, SSR guards, and the perf checklist below.

If the user asks for code targeting a framework this project does not run, refuse and offer prompt mode. Foreign framework components will not run here.

## Build workflow

1. **Clarify scope sparingly**, two questions max: theme/tone, primary 3D object, key interactions (hover/scroll/click), single page vs multi-section. Ask for a reference image if none was given: screenshots raise fidelity dramatically.
2. **Confirm deps.** Check `package.json` for `@react-three/fiber`. If missing, add `three`, `@react-three/fiber`, `@react-three/drei`, and dev-dep `@types/three` with the project's package manager.
3. **Wire SSR-safely.** Three.js touches `window`; server rendering crashes without a guard. Mount the Canvas behind a client-only wrapper (render `null` until after hydration), and exclude three packages from SSR externalization in the bundler config. Required, not optional.
4. **Build the hero scene first.** Pick the closest archetype (floating hero object, tilt cards, scroll-driven explorer, premium showcase) and get it rendering before expanding to other sections.
5. **Perf pass before declaring done**: `dpr={[1, 2]}` cap, `frameloop="demand"` for static scenes, lazy-load GLTF, `<Suspense>` fallback on every async asset, touch controls on mobile, `prefers-reduced-motion` fallback.
6. **Verify** per the section below.

## Guardrails

- **Never** render `<Canvas>` at module scope or in a server-rendered route body without the client-only wrapper. The reconciler calls `window`/`document` at mount.
- **Never** load `.gltf`, `.hdr`, or large textures without `<Suspense fallback={...}>`. The route flashes blank otherwise.
- **Never** ship without a `prefers-reduced-motion` branch that stills the animation and shows a static frame.
- Cap DPR at 2. Uncapped DPR tanks retina and mobile FPS.
- Camera FOV 35-50 for product and hero shots; 60-75 only for immersive explorers.

## Good vs bad

Bad: import the Canvas component at the top of a server-rendered route, ship, and discover "window is not defined" only in production builds.

Good: a `ClientOnly` wrapper that returns `null` until mounted, Canvas inside it with `dpr={[1, 2]}`, GLTF behind Suspense with a branded loading fallback.

## Verification

Run the dev server and load the route. Expect no SSR crash, no console errors, and no "Hooks can only be used within the Canvas" warnings. If the SSR crash appears, the Canvas is mounting during server render: add the client-only wrapper and bundler SSR exclusions.

Then open the route at a 375px viewport and take a screenshot. Expect the scene rendered, interactive via touch, and legible. If the scene is blank on mobile only, check DPR cap and texture sizes first.

## Completion checklist

- [ ] Project build succeeds
- [ ] Route loads with no `window is not defined` SSR crash
- [ ] Console clean, no R3F hook warnings
- [ ] 375px viewport renders and responds to touch
- [ ] Reduced-motion users see a still frame, not a frozen loading state
- [ ] All async assets behind Suspense with a real fallback
- [ ] DPR capped, static scenes on `frameloop="demand"`

Any box unchecked: not done. Fix or say so.

## Footguns

- **Canvas at module scope.** Crashes SSR at import time. Fix: client-only wrapper plus SSR exclusion for three packages.
- **GLTF without Suspense.** Blank flash, sometimes a permanent blank on slow networks. Fix: `<Suspense fallback>` around every loader.
- **Uncapped DPR.** 3x pixel ratio on phones quarters the frame rate. Fix: `dpr={[1, 2]}`.
- **Reduced-motion ignored.** Accessibility failure and a common audit rejection. Fix: media-query branch that freezes the animation loop with a composed static frame.
