Build a designer-portfolio landing page in React + Tailwind v4 + Framer Motion + @react-three/drei.
HERO
-
Full-bleed hero (no rounded box, edge-to-edge): wrapper is w-full with inline style background: radial-gradient(125% 125% at 50% 90%, #fff 40%, #63e 100%).
-
Inside the hero, render floating 3D clouds with @react-three/drei's inside . Use 3 seeds at low/wide positions, slow speed (0.16–0.20), volume ~2.2–2.6, segments={40}, color #fff, opacity 1, fade 0. Canvas camera position [0,0,10], fov 45, alpha true, antialias true, pointer-events none.
-
The bottom of the gradient must FADE into the white page below, no hard cut. Use an absolutely-positioned overlay: pointer-events-none absolute inset-x-0 bottom-0 h-40 z-[1] bg-gradient-to-b from-transparent to-white.
-
Hero text content sits at z-30 with relative positioning so it stays above the fade.
-
Hero copy: huge headline (text-4xl md:text-6xl lg:text-[72px] font-semibold tracking-tight), short subhead, and two CTAs side-by-side.
NAVBAR
-
Floating dark pill nav, fixed top-8 left-1/2 -translate-x-1/2 z-50.
-
Inner pill: flex items-center gap-6 bg-stone-900 text-white rounded-full px-6 py-3 shadow-2xl shadow-stone-900/20.
-
Contents: logo on left, then a hidden-on-mobile group of links (Work, Why us, Plans, Testimonials) anchoring to #work, #why, #plans, #testimonials. All links text-white with hover:opacity-80 transition-opacity.
-
NO Book-a-call button visible by default.
SCROLL-TRIGGERED BOOK-A-CALL
-
useState scrolled, useEffect with passive scroll listener that sets scrolled = window.scrollY > 200.
-
After the links, render a motion.div wrapper:
-
className: flex items-center gap-3 overflow-hidden pr-1
-
initial={false}
-
animate={{ width: scrolled ? "auto" : 0, marginLeft: scrolled ? 0 : -24, opacity: scrolled ? 1 : 0 }}
-
transition={{ duration: 0.4, ease: [0.4, 0.36, 0, 1] }}
-
style={{ willChange: "width, margin, opacity" }}
-
Inside the wrapper: a thin vertical divider and the Book-a-call using btnSecondary with !px-4 !py-1.5 !text-sm whitespace-nowrap shrink-0.
-
Effect: as the user scrolls, the navbar smoothly opens, the divider appears, and the button slides out from behind it. Reverse on scroll up.
REUSABLE BUTTON PRIMITIVES (define once, use everywhere)
- btnPrimary (black pill, shimmer):
"group relative isolate inline-flex items-center justify-center overflow-hidden font-semibold transition duration-300 ease-[cubic-bezier(0.4,0.36,0,1)] before:duration-300 before:ease-[cubic-bezier(0.4,0.36,0,1)] before:transition-opacity rounded-full shadow-[0_1px_theme(colors.white/0.07)_inset,0_1px_3px_theme(colors.gray.900/0.2)] before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:rounded-full before:bg-gradient-to-b before:from-white/20 before:opacity-50 hover:before:opacity-100 after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:rounded-full after:bg-gradient-to-b after:from-white/10 after:from-[46%] after:to-[54%] after:mix-blend-overlay text-sm px-6 py-3.5 ring-1 bg-black text-white ring-black"
- btnSecondary (white pill, inset shadow):
"inline-flex items-center justify-center overflow-clip rounded-full px-6 py-3.5 font-semibold text-sm text-stone-950 shadow-[0_2px_3px_-1px_theme(colors.black/0.08),0_0_0_0.5px_theme(colors.gray.950/0.18),0_1px_0_0_theme(colors.white/0.10)_inset] [background:linear-gradient(180deg,rgba(19,19,22,0)_45%,rgba(19,19,22,0.03)_55%),#fff] hover:brightness-[0.97] transition-all"
- Use these for ALL CTAs: hero (Book a call = primary, View work = secondary), plans (highlighted plan = secondary, other = primary), bottom CTA (Book a call = primary, DM me on X = secondary), navbar Book-a-call (secondary, smaller).
SIGNATURE PILL
- Below the hero CTAs, a small "Working with startups and teams since 2019" pill:
className="relative z-30 text-black/60 text-sm mt-16 inline-block border border-white rounded-full px-4 py-2 bg-white/70 backdrop-blur-sm"
- z-30 keeps it above the fade overlay; backdrop-blur-sm keeps it readable.
MARQUEE
-
Below the hero gradient, an infinite-scrolling row of client name pills.
-
motion.div animate={{ x: ["0%", "-50%"] }} transition={{ duration: 18, ease: "linear", repeat: Infinity }} className="flex gap-4 whitespace-nowrap".
-
Each pill: text-stone-400 text-sm font-medium px-4 py-2 rounded-full border border-stone-200.
-
Wrapper: max-w-[1100px] mx-auto mt-2 overflow-hidden.
SPACING
-
Hero inner content: pt-40 md:pt-52 pb-10 md:pb-12 px-6 (so headline clears the floating nav).
-
Gradient bottom margin mb-4, marquee mt-2 (tight gap, no dead space).
STACK
- React 19, Tailwind v4 (@tailwindcss/vite), Framer Motion (import { motion } from "motion/react"), @react-three/fiber, @react-three/drei, lucide-react for icons. Use clsx + tailwind-merge via a cn() helper.
Drei clouds docs: https://drei.docs.pmnd.rs/abstractions/clouds