# Prompt I used to build a 3D soda landing page with Grok 4.5 in one shot

> Source: <https://gist.github.com/StarKnightt/215b05092c898ead0530a61c2b685e62>
> Published: 2026-07-11 16:20:02+00:00

| # 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 loading screen needed |
