Three.js From Zero · Article s13-12

R3F Portfolio Site

R3F Portfolio Site is Article s13-12 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.

← Three.js From ZeroS13-12 · R3F Mastery

Season 13 · Article 12 · R3F Mastery

The capstone: a deployable Three.js portfolio site. Scroll-driven story, project cards in 3D space, contact form, lighthouse 90+. Everything from Season 13 working together to ship a real artifact.

Capstone build

Final structure + the patterns that make this stand out from the 500 generic Three.js portfolios already on the web.

The information architecture

Three full-viewport sections, each anchored by a 3D centerpiece:

  1. Hero — your name in 3D text, slow camera dolly
  2. Projects — 5-8 project cards orbiting a center, click to expand
  3. Contact — a 3D mailbox + form

Scroll drives the camera through them. One canvas, three "scenes" composed in a single Three.js scene.

Tech stack

  • Vite + React + TypeScript
  • @react-three/fiber + drei
  • @react-three/postprocessing (bloom on your name and emissive accents)
  • Framer Motion for HTML transitions
  • Vercel / Cloudflare Pages for deploy

The hero

import { Text3D, Center, Float } from '@react-three/drei';

function Hero() {
  return (
    <Float speed={1.5} rotationIntensity={0.3} floatIntensity={0.5}>
      <Center>
        <Text3D font="/inter-bold.json" size={1.2} height={0.3} bevelEnabled bevelSize={0.03}>
          Your Name
          <meshStandardMaterial color="#ec4899" metalness={0.7} roughness={0.2}
            emissive="#ec4899" emissiveIntensity={1} toneMapped={false} />
        </Text3D>
      </Center>
    </Float>
  );
}

The project carousel

function Projects() {
  const projects = useMemo(() => [/* your data */], []);
  return (
    <group position={[0, -10, 0]}>
      {projects.map((p, i) => {
        const angle = (i / projects.length) * Math.PI * 2;
        return (
          <ProjectCard key={p.id} project={p}
            position={[Math.cos(angle) * 4, 0, Math.sin(angle) * 4]}
            rotation={[0, -angle, 0]} />
        );
      })}
    </group>
  );
}

Scroll-driven camera

function CameraRig() {
  const { camera } = useThree();
  useFrame(() => {
    const t = scrollFraction.current;
    const yTarget = -t * 20;        // 0 to -20 over full scroll
    camera.position.y += (yTarget - camera.position.y) * 0.06;
    camera.lookAt(0, yTarget, 0);
  });
  return null;
}

Lighthouse-90 checklist

The wall portfolios hit:

  • preload critical assets — useGLTF.preload in app entry
  • compress glb models — Draco + KTX2 (S6-02, S6-03)
  • throttle for mobile — drop shadow map size, fewer particles, lower DPR cap
  • SSR the HTML — Astro or Next.js renders the page shell instantly; React + R3F hydrate after
  • defer postprocessing — load EffectComposer chunk on idle, not on first frame

Common first-time pitfalls

"Site is slow on mobile." You're not detecting mobile and downgrading. Check window.innerWidth, set Canvas dpr to [1, 1.5], disable shadows, drop particle counts.
"Bounce rate is terrible." First paint is 5+ seconds. The page is "Loading…" for too long. Render HTML shell immediately, lazy-load the 3D bundle, preload models on viewport-near.
"Recruiter can't find my contact info." 3D-only contact = unreachable. Always include a plain HTML mailto link in the footer for the people who don't scroll.

Ship checklist

  1. Domain bought (yourname.dev)
  2. Lighthouse: Performance 90+, Accessibility 100, Best Practices 100
  3. OpenGraph + Twitter card image (your name in 3D, screenshotted)
  4. Analytics: PostHog or Plausible (privacy-respecting)
  5. Sitemap.xml + robots.txt
  6. Test on iPhone SE, Android low-end, Quest browser
  7. Posted on X, HN ("Show HN"), r/threejs, awwwards.com

You've finished R3F Mastery

Twelve articles. From "what is JSX as scene graph" to a deployable portfolio. You now know enough R3F to take any vanilla three.js demo (including Bruno's) and port it. You can use this season as your reference forever.