Entrada de Anthropic para prompt de UI/UX, preservada en la biblioteca de prompts de Eldergenix. El cuerpo original se mantiene sin traducir para conservar la procedencia y la evidencia de origen.
# Wanderful — Cinematic Hero Section
## Overview
Build a full-viewport cinematic hero section for a travel brand called "Wanderful". A fixed, full-screen looping background video sits behind a fixed header, a centered two-line headline, and a bottom content block, all rendered over a black background. The video reacts to the cursor with a smooth GSAP-driven parallax drift, and the headline and bottom block fade in on mount.
## Tech Stack
- **Framework:** React 18 (`react` `^18.3.1`, `react-dom` `^18.3.1`) with TypeScript (`typescript` `^5.6.3`)
- **Build tool:** Vite (`vite` `^5.4.11`) with `@vitejs/plugin-react` `^4.3.4`
- **Styling:** Tailwind CSS (`tailwindcss` `^3.4.15`) with `postcss` `^8.4.49` and `autoprefixer` `^10.4.20`
- **Animation:** GSAP (`gsap` `^3.12.5`) — used for the mouse-parallax `gsap.set` on each `requestAnimationFrame`
- **Icons:** Lucide (`lucide-react` `^0.468.0`) — the `Lock` icon
- **Fonts:** Inter (headings + root font family), Barlow (body), Instrument Serif, plus a custom display font "Dirtyline" loaded via `@font-face`
- **Notable techniques:** fixed full-screen video background, over-scaled wrapper for edge-safe parallax, lerped mouse parallax inside a `requestAnimationFrame` loop, custom "liquid glass" CSS utility, double-`requestAnimationFrame` mount transition
## Global Setup
### Root container (`App.tsx`)
- Classes: `min-h-screen overflow-x-hidden bg-black text-white`
- Inline style: `fontFamily: "'Inter', sans-serif"`
- Renders, in order: `<VideoBackground />`, `<Header />`, and a `<main>` containing `<HeroHeadline mounted={mounted} />` and `<BottomBlock mounted={mounted} />`.
### Mount transition state
The hero uses a `mounted` boolean (`useState(false)`) that flips to `true` after two nested `requestAnimationFrame` calls. The double rAF guarantees the pre-transition styles are painted first, so the opacity/translate transition reliably plays on mount. Clean up both rAF handles on unmount.
```tsx
const [mounted, setMounted] = useState(false)
useEffect(() => {
let raf2 = 0
const raf1 = requestAnimationFrame(() => {
raf2 = requestAnimationFrame(() => setMounted(true))
})
return () => {
cancelAnimationFrame(raf1)
cancelAnimationFrame(raf2)
}
}, [])
```
### `index.html`
- `lang="en"`, `<meta charset="UTF-8" />`, `<meta name="viewport" content="width=device-width, initial-scale=1.0" />`
- Description meta: `Wanderful — smart itineraries that shape around you. Venture without edges.`
- Title: `Wanderful — Venture without edges.`
- Inline SVG data-URI favicon (a white circle outline with a white compass-needle path):
```html
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='14' fill='none' stroke='white' stroke-width='2'/%3E%3Cpath d='M21 11l-3.5 6.5L11 21l3.5-6.5z' fill='white'/%3E%3C/svg%3E"
/>
```
- Mount point `<div id="root"></div>` with `<script type="module" src="/src/main.tsx"></script>`.
### Entry point (`src/main.tsx`)
```tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
```
## Fonts & Base Styles (`src/index.css`)
Load Google Fonts and a custom display font at the top of `src/index.css`:
```css
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Barlow:wght@300;400;500;600&family=Inter:wght@300;400;500;600;700&display=swap');
@font-face {
font-family: 'Dirtyline';
src: url('https://fonts.cdnfonts.com/s/15011/Dirtyline36DaysofType.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: 'Barlow', sans-serif;
background: #000;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::selection {
background: rgba(255, 255, 255, 0.9);
color: #000;
}
```
- **Body font:** Barlow. **Hero headings / root font family:** Inter. **Body background:** `#000`.
### Liquid-glass utility
Add this `.liquid-glass` utility (and its `::before` pseudo-element) to `index.css`. It is used by the navbar and the "Get roaming" button.
```css
.liquid-glass {
background: rgba(255, 255, 255, 0.01);
background-blend-mode: luminosity;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
border: none;
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
}
.liquid-glass::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1.4px;
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.45) 0%,
rgba(255, 255, 255, 0.15) 20%,
rgba(255, 255, 255, 0) 40%,
rgba(255, 255, 255, 0) 60%,
rgba(255, 255, 255, 0.15) 80%,
rgba(255, 255, 255, 0.45) 100%);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
```
## Video Background (`src/components/VideoBackground.tsx`)
A fixed, full-screen video backdrop with a GSAP-driven mouse parallax. The wrapper is over-scaled (`scale-[1.08]`) so the lerped x/y drift never exposes the viewport edges.
### Structure
- Outer wrapper: `fixed inset-0 z-0` with `aria-hidden="true"`.
- Parallax wrapper (`ref`): `h-full w-full origin-center scale-[1.08]`.
- `<video>`: `h-full w-full object-cover`, attributes `autoPlay muted loop playsInline`.
- On `onLoadedMetadata`, set `e.currentTarget.playbackRate = 1.25`.
### Video source
The source is a local vendored asset:
```ts
const VIDEO_SRC =
'/assets/hf_20260510_060007_60275ce7-030c-4668-a160-8f364ec537d3.mp4'
```
> The video file lives at `public/assets/hf_20260510_060007_60275ce7-030c-4668-a160-8f364ec537d3.mp4`. It was originally sourced from the CloudFront URL below and vendored locally (scheme + host lowercased; path kept verbatim from the source asset name): `https://d8j0ntlcm91z4.cloudfront.net/user_38xzzbokvigwjottwixh07lwa1p/hf_20260510_060007_60275ce7-030c-4668-a160-8f364ec537d3.mp4`
### Mouse parallax
Listen to `mousemove` on `window`. Compute the target offset relative to the viewport center, then lerp toward it inside a `requestAnimationFrame` loop and apply via `gsap.set`:
- `cx = window.innerWidth / 2`, `cy = window.innerHeight / 2`
- `targetX = ((e.clientX - cx) / cx) * 20`, `targetY = ((e.clientY - cy) / cy) * 20`
- Each tick: `currentX += (targetX - currentX) * 0.06`, same for `currentY`
- `gsap.set(videoBg, { x: currentX, y: currentY })`
```tsx
useEffect(() => {
const videoBg = parallaxRef.current
if (!videoBg) return
let targetX = 0
let targetY = 0
let currentX = 0
let currentY = 0
let rafId = 0
const onMouseMove = (e: MouseEvent) => {
const cx = window.innerWidth / 2
const cy = window.innerHeight / 2
targetX = ((e.clientX - cx) / cx) * 20
targetY = ((e.clientY - cy) / cy) * 20
}
const tick = () => {
currentX += (targetX - currentX) * 0.06
currentY += (targetY - currentY) * 0.06
gsap.set(videoBg, { x: currentX, y: currentY })
rafId = requestAnimationFrame(tick)
}
window.addEventListener('mousemove', onMouseMove)
rafId = requestAnimationFrame(tick)
return () => {
window.removeEventListener('mousemove', onMouseMove)
cancelAnimationFrame(rafId)
}
}, [])
```
## Header (`src/components/Header.tsx`)
Fixed top bar: `fixed inset-x-0 top-0 z-50 flex items-center justify-between px-10 py-8`.
- **Left — wordmark:** anchor to `/`, text `Wanderful` followed by `<sup>TM</sup>`. Classes: `text-[17px] font-semibold tracking-tight`.
- **Center — nav:** a `<nav>` with `liquid-glass hidden items-center gap-1 rounded-full px-2 py-2 md:flex` (hidden below `md`, flex at `md` and up). Links derived from `['JOURNEY', 'BENEFITS', 'JOURNAL', 'GUIDEBOOK']`, each `href` being `#${link.toLowerCase()}`. Each link: `rounded-full px-4 py-1.5 text-[11px] font-medium tracking-[0.12em] text-white/90 transition-colors duration-200 hover:text-white`. Visible labels: `JOURNEY`, `BENEFITS`, `JOURNAL`, `GUIDEBOOK`.
- **Right — CTA:** anchor `href="#get-roaming"` with `liquid-glass rounded-full px-5 py-2.5 text-[11px] font-medium tracking-[0.12em] text-white/90 transition-colors duration-200 hover:text-white`. Label: `GET ROAMING`.
## Hero Headline (`src/components/HeroHeadline.tsx`)
Receives a `mounted: boolean` prop. Renders a fixed, centered `<h1>`.
- **Classes:** `fixed inset-x-0 z-20 px-6 text-center transition-all duration-1000`, plus the mount toggle: `translate-y-0 opacity-100` when mounted, otherwise `translate-y-6 opacity-0`.
- **Inline styles:** `top: '120px'`, `fontFamily: "'Inter', sans-serif"`, `fontWeight: 400`, `fontSize: 'clamp(40px, 5.4vw, 72px)'`, `lineHeight: 1.1`, `letterSpacing: '-0.02em'`.
- **Line 1 (white):** `<span className="block text-white">` — `Venture without edges.`
- **Line 2 (`rgba(255,255,255,0.55)`):** `<span className="block" style={{ color: 'rgba(255,255,255,0.55)' }}>` — `Uncover with keen instinct.`
**Fade-in on mount:** opacity `0 → 100` and `translate-y-6 → 0` via `transition-all duration-1000`.
## Bottom Block (`src/components/BottomBlock.tsx`)
Receives a `mounted: boolean` prop. Imports `Lock` from `lucide-react`.
- **Container:** `fixed inset-x-0 bottom-14 z-20 flex flex-col items-center gap-6 px-6 transition-all delay-300 duration-1000`, plus the mount toggle: `translate-y-0 opacity-100` when mounted, otherwise `translate-y-6 opacity-0` (fade-in with `delay-300`).
1. **Paragraph:** `max-w-[620px] text-center text-[15px] leading-relaxed`, composed of two spans:
- White span (`text-white`): `Our smart itineraries shape around you — your rhythm, your vibe, your hunger for adventure.` (the em dash is `—`)
- Muted span (`text-white/55`), preceded by a literal space (`{' '}`): `Each getaway is tailored, seamless, and wholly yours.`
2. **Button:** `type="button"`, classes `rounded-full bg-white px-8 py-3.5 text-[15px] font-medium text-black transition-all duration-300 hover:scale-[1.03] hover:shadow-[0_0_32px_4px_rgba(255,255,255,0.2)] active:scale-[0.97]`. Label: `Plan my escape today`.
3. **Trust row:** `flex items-center gap-2 text-white/70` containing the `Lock` icon (`size={13} strokeWidth={1.5}`) and a `<span className="text-[11px] font-medium tracking-[0.14em]">` with text: `SECURE BY DESIGN. ZERO DATA LEAKS.`
## Animations Summary
- **Mount fade-in (headline + bottom block):** `opacity 0 → 100` and `translate-y-6 → 0`, `transition-all duration-1000`. Bottom block adds `delay-300`.
- **Mouse parallax (video):** lerped offset (`* 0.06` per frame) toward a target of up to `± 20px` in each axis, applied with `gsap.set` inside `requestAnimationFrame`.
- **Button interactions:** `hover:scale-[1.03]` + `hover:shadow-[0_0_32px_4px_rgba(255,255,255,0.2)]`, `active:scale-[0.97]`, `transition-all duration-300`.
- **Nav/CTA link hover:** `text-white/90` → `hover:text-white`, `transition-colors duration-200`.
## Color Palette
- **Background:** black (`bg-black` / `#000`)
- **Primary text:** white
- **Muted headline line:** `rgba(255,255,255,0.55)`
- **Muted paragraph span:** `text-white/55`
- **Nav/CTA text:** `text-white/90` → `text-white` on hover
- **Trust row text:** `text-white/70`
- **Selection:** background `rgba(255, 255, 255, 0.9)`, color `#000`
- **Button:** white background, black text; hover glow `rgba(255,255,255,0.2)`
- **Liquid-glass:** base `rgba(255, 255, 255, 0.01)`, inset highlight `rgba(255, 255, 255, 0.1)`, gradient border stops between `rgba(255,255,255,0.45)` and `rgba(255,255,255,0)`
## Tailwind Configuration (`tailwind.config.js`)
```js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
```
## File Structure
```
wanderful-cinematic-hero/
├── index.html
├── package.json
├── tailwind.config.js
├── public/
│ └── assets/
│ └── hf_20260510_060007_60275ce7-030c-4668-a160-8f364ec537d3.mp4
└── src/
├── main.tsx
├── App.tsx
├── index.css
├── vite-env.d.ts
└── components/
├── VideoBackground.tsx
├── Header.tsx
├── HeroHeadline.tsx
└── BottomBlock.tsx
```
## Dependencies
- **Runtime:** `gsap`, `lucide-react`, `react`, `react-dom`
- **Dev:** `@types/react`, `@types/react-dom`, `@vitejs/plugin-react`, `autoprefixer`, `playwright-core`, `postcss`, `tailwindcss`, `typescript`, `vite`
- Tailwind configured with content globs `./index.html` and `./src/**/*.{js,ts,jsx,tsx}`.