| # Recreate this interactive 3D soda landing page in Next.js + React + TypeScript + Tailwind | |
| # Design and 3D assets from getlayers.ai (Soda template). This prompt recreates the design using Grok 4.5. | |
| You are an expert creative front-end developer. Build a Next.js (App Router) project with React, TypeScript, and Tailwind CSS that reproduces the design below exactly β same layout, visuals, motion, and interaction. | |
| Use pnpm as package manager. Use GSAP for animations and Google's @google/model-viewer for 3D models. All 3D assets are hosted on a CDN (URLs below). The page should be a single full-viewport (no-scroll) hero landing page. | |
| ## Project Setup | |
| bash | | | pnpm create next-app@latest soda-landing --typescript --tailwind --app --eslint | | | cd soda-landing | | | pnpm add gsap | | | pnpm add @google/model-viewer | | | | |
| ## What it is | |
| A full-viewport (no-scroll) hero landing page for a fictional "Diet Soda" beverage. A dark radial gradient background (teal for the default "Classic" flavor, blue for the "Zero Lime" flavor) fills the screen. A large 3D soda can floats in the center, rendered with <model-viewer>, and tilts toward the cursor in real time. Around it, dozens of 3D cherry/leaf models float and drift with parallax, and are repelled by the pointer like a force field. Translucent PNG bubbles rise endlessly from the bottom. A glassmorphism header sits on top; a left column has a giant cursive headline + CTA + award badge; a right column has a two-card flavor carousel and a second headline. | |
| Clicking a flavor card triggers a choreographed transition: the background color morphs, the can spins 720Β° with a motion blur and swaps its base-color texture at the peak, and all the berry models implode toward center, swap their model (cherry β blueberry), then explode back out to new random positions. | |
| ## Fonts | |
| Add to layout.tsx via next/font/google: | |
| - Body: Inter (weights 400, 500) | |
| - Headings: Galada (cursive, weight 400) | |
| - Nav: Manrope (weights 400, 700) | |
| - Logo/Headings alt: Outfit (weights 400, 700, 900) | |
| ## Color Palette | |
| - Near-black: #0a0a0a | |
| - Classic teal: inner #0b8a78, mid #044e3b, outer #011411 | |
| - Blue theme: inner #0b4f8a, mid #04294e, outer #010c14 | |
| - Pink accent: #fbcfe8 | |
| - Accent text on pink: #011d17 | |
| - Glass bg: rgba(255, 255, 255, 0.05) | |
| - Glass border: rgba(255, 255, 255, 0.1) | |
| - Muted text: rgba(255, 255, 255, 0.7) | |
| ## Component Structure | |
| | | | app/ | | | layout.tsx β fonts, metadata, global styles | | | page.tsx β main page component | | | globals.css β custom CSS (gradients, animations, model-viewer styles) | | | components/ | | | Header.tsx β glassmorphism nav bar | | | HeroLeft.tsx β "Pure Zero" title, description, CTA, award badge | | | HeroRight.tsx β flavor carousel cards, "Refreshingly Clean" title | | | ProductViewer.tsx β center 3D can with model-viewer | | | FloatingBerries.tsx β foreground + background berry model-viewers | | | FloatingLeaves.tsx β background leaf model-viewers | | | BubblesContainer.tsxβ rising bubble animation | | | FlavorCard.tsx β individual flavor card | | | | |
| ## Page Layout | |
| The page does NOT scroll. overflow: hidden; height: 100vh; on body. | |
| The body background is a radial gradient that transitions between teal (Classic) and blue (Zero Lime) themes using GSAP CSS variable animation. | |
| The hero is a flex row spanning the viewport. Inside it (in DOM order): floating leaves (far background), left column, background berries, center product (3D can), foreground berries, right column. | |
| ## Key Interactions | |
| ### 1. Mouse cursor tilt on 3D can | |
| Per animation frame, smoothly interpolate mouse position and update model-viewer camera orbit: | |
| | | | cameraOrbit = `${(currentMouse.x * 40) + switchSpin}deg ${90 + (currentMouse.y * 20)}deg 380%` | | | | |
| Mouse smoothing lerp factor: 0.05 | |
| ### 2. Parallax on floating elements | |
| - Foreground berries: translate(mouse.x * 60px, mouse.y * 60px) | |
| - Background berries: translate(mouse.x * -30px, mouse.y * -30px) | |
| - Leaves: translate(mouse.x * -15px, mouse.y * -15px) | |
| ### 3. Berry pointer repulsion | |
| Radius: 400px, strength: -80, lerp: 0.1, spin: angle += 0.2 * speedMult, speedMult = 1 + force * 5. Berry float amplitude: 15px Y / 6deg. Per-index durations: [5, 7, 6, 8, 5.5, 6.5, 9, 11, 10]. | |
| ### 4. Flavor switch choreography | |
| On card click: | |
| 1. Background morph: GSAP animates CSS variables --bg-inner, --bg-mid, --bg-outer over 1.5s ease power2.inOut | |
| 2. Can spin: Phase 1 β 360deg, blur 15px, 0.6s, power2.in. At peak, swap texture. Phase 2 β 720deg, blur 0, 1.5s, back.out(0.7) | |
| 3. Berry implode/explode: Implode to center 0.5s power2.in, scale 0.1 opacity 0. Hold 0.3s. Swap .src (cherry β blueberry). Explode to random Β±100px, 0.9s back.out(1.5) | |
| ### 5. Rising bubbles | |
| Spawn bubble <img> every 400ms. Size: 10-30px, opacity: 0.2-0.6, rise duration: 4-10s, drift +30px X, 360deg rotation over -110vh. | |
| ### 6. Floating leaves | |
| Amplitude: 20px Y, 15px X, 15deg rotation. Durations: 10 + i*2 seconds. | |
| ## model-viewer Setup | |
| Since @google/model-viewer is a web component, in Next.js you need to handle it as a client component. Create a wrapper: | |
| tsx | | | 'use client' | | | import '@google/model-viewer' | | | // Declare the custom element for TypeScript | | | declare global { | | | namespace JSX { | | | interface IntrinsicElements { | | | 'model-viewer': any | | | } | | | } | | | } | | | | |
| Center can settings: | |
| - camera-orbit="0deg 90deg 380%" | |
| - field-of-view="30deg" | |
| - exposure="1.5" | |
| - environment-image="neutral" | |
| - camera-controls disable-zoom shadow-intensity="0" | |
| - CSS: transform: translate(-50%, -50%) rotate(25deg), size 80vw Γ 80vh | |
| - --progress-bar-color: transparent; --poster-color: transparent; | |
| ## Texture Swapping | |
| On model load, preload both textures using modelViewer.createTexture(). Do a one-frame shader warm-up (apply blue then green) so first real swap is instant. | |
| ts | | | const blueTexture = await modelViewer.createTexture(ASSETS.BLUE_BASE_COLOR) | | | const greenTexture = await modelViewer.createTexture(ASSETS.GREEN_BASE_COLOR) | | | | |
| On flavor switch, apply via: | |
| ts | | | material.pbrMetallicRoughness.baseColorTexture.setTexture(texture) | | | | |
| ## Header | |
| Glassmorphism nav bar with: | |
| - Logo: SVG circle + cross icon with "Soda" text in Galada font | |
| - Nav pills: Home (active), Ingredients, Taste, Eco, Reviews | |
| - Active/hover state: bg-[#fbcfe8] text-[#011d17] | |
| - Contact Us button: bg-black/50 rounded pill | |
| - Glass effect: backdrop-blur-[20px] bg-white/8 border border-white/20 | |
| ## Left Column | |
| - Main title: "Pure" (outline) + "Zero" in Galada, clamp(5rem, 10vw, 12rem), line-height: 0.8 | |
| - Description: "Unleash the crisp taste of zero sugar..." in Inter, muted color | |
| - CTA: "Shop Now" pill button with pink + circle icon | |
| - Award badge: glass icon + "DESIGN AWARDS" / "PREMIUM BEVERAGE 2025" | |
| - Award badge pushed to bottom with margin-top: auto | |
| ## Right Column | |
| - Two flavor cards: "Diet Classic" ($2.99) and "Zero Lime" ($2.99) | |
| - Cards: glass bg, border-radius: 28px, hover lifts image -30px rotate(-12deg) scale(1.15) | |
| - Active card: border-color: #fbcfe8 | |
| - Zero Lime card image: filter: brightness(0.7) | |
| - Carousel nav arrows: glass circles with β β | |
| - Side title: "Refreshingly" (outline) + "Clean" in Galada, right-aligned | |
| ## Berry Positions | |
| Foreground (z-index above text and can): | |
| - b1: 220px at top: 25%; left: 30% | |
| - b2: 100px at top: 60%; left: 42% | |
| - b3: 250px at top: 30%; left: 62% | |
| - b4: 140px at top: 15%; left: 48% | |
| - b5: 120px at top: 75%; left: 20% | |
| - b6: 180px at top: 45%; left: 75% | |
| Background (behind everything): | |
| - b7: 80px at top: 15%; left: 40% opacity 0.7 | |
| - b8: 70px at top: 50%; left: 55% opacity 0.6 | |
| - b9: 75px at top: 80%; left: 35% opacity 0.7 | |
| ## Leaf Positions | |
| - l1: 60px at top: 10%; left: 15% | |
| - l2: 140px at top: 40%; left: 80% opacity 0.4 | |
| - l3: 80px at top: 70%; left: 75% | |
| - l4: 120px at top: 85%; left: 20% opacity 0.3 | |
| ## Responsive (max-width: 1200px) | |
| - Product viewer: width: 100vw; height: 60vh; top: 40% | |
| - Hero content: single column, padding-top: 8rem | |
| - Titles: font-size: 5rem | |
| - Right column: center-aligned | |
| ## Assets | |
| All assets are hosted on a CDN. Create a constants file: | |
| ts | | | // lib/assets.ts | | | export const ASSETS = { | | | LEAVES: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/leaves.glb', | | | CHERRY: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/cherry.glb', | | | BLUEBERRY: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/blueberry.glb', | | | SODA_CAN: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/deit_soda2.glb', | | | GREEN_SODA_IMG: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/Green%20Soda.png', | | | BLUE_SODA_IMG: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/Blue%20Soda.png', | | | GREEN_BASE_COLOR: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/green%20base%20color.jpg', | | | BLUE_BASE_COLOR: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/blue%20base%20color.jpg', | | | BUBBLE: 'https://api.getlayers.ai/storage/v1/object/public/public/assets/soda-14ff8a788d/bubble.png', | | | } as const | | | | |
| ## Animation Loop (requestAnimationFrame) | |
| Run a single requestAnimationFrame loop that handles: | |
| 1. Mouse interpolation (lerp 0.05) | |
| 2. Can camera orbit update (mouse tilt + switch spin) | |
| 3. Parallax container transforms | |
| 4. Berry repulsion + floating (only when not switching flavors) | |
| 5. Leaf floating | |
| Use useEffect with cleanup in the main page component or a custom useAnimationLoop hook. | |
| ## Important Notes | |
| - All model-viewer components must be in 'use client' components | |
| - URL-encode spaces in asset filenames (%20) | |
| - The can entrance: .hero-center starts opacity: 0, fades in over 1.5s with 0.3s delay, then floats Β±20px infinitely | |
| - Hide model-viewer defaults: --progress-bar-color: transparent; --poster-color: transparent | |
| - Preload berry models in a hidden div to avoid lag during flavor switch | |
| - No separate screen needed |
PassionQA: Turning My Passion for Software Quality into AI-Powered Test Intelligence