Animated portfolio website
Build a single-page dark portfolio landing page using React + Vite + Tailwind CSS + TypeScript + GSAP + Framer Motion + hls.js.
Prompt
Build a single-page dark portfolio landing page using React + Vite + Tailwind CSS + TypeScript + GSAP + Framer Motion + hls.js.
Global Design System
Fonts
Google Fonts import: Inter (300–700) and Instrument Serif (italic, 400).
--font-body: 'Inter', sans-serif→ Tailwindfont-body--font-display: 'Instrument Serif', serif→ Tailwindfont-display
CSS Custom Properties (HSL, no hsl() wrapper — Tailwind adds it)
--bg: 0 0% 4%; /* #0a0a0a */
--surface: 0 0% 8%; /* #141414 */
--text: 0 0% 96%; /* #f5f5f5 */
--muted: 0 0% 53%; /* #888888 */
--stroke: 0 0% 12%; /* #1f1f1f */
--accent: 0 0% 96%;
Tailwind Custom Colors
bg: "hsl(var(--bg))",
surface: "hsl(var(--surface))",
"text-primary": "hsl(var(--text))",
muted: "hsl(var(--muted))",
stroke: "hsl(var(--stroke))",
Accent Gradient
linear-gradient(90deg, #89AACC 0%, #4E85BF 100%) — used on logo ring, hover borders, progress bars. CSS utility class .accent-gradient.
Custom Animations (in index.css)
@keyframes scroll-down— translateY(-100%) → translateY(200%), 1.5s ease-in-out infinite@keyframes role-fade-in— opacity 0 + translateY(8px) → opacity 1 + translateY(0), 0.4s ease-out@keyframes gradient-shift— background-position 0% 50% → 100% 50% → 0% 50%, 6s ease infinite (for animated gradient borders)
Forced dark theme — no light mode toggle. body gets bg-bg text-text-primary.
Page Structure (Index.tsx)
{isLoading && setIsLoading(false)} />}
Section 1: Loading Screen
Full-screen overlay (fixed inset-0 z-[9999] bg-bg). Uses requestAnimationFrame counter from 000→100 over 2700ms.
- Top-left: "Portfolio" label —
text-xs text-muted uppercase tracking-[0.3em]. Animates y:-20→0, opacity 0→1. - Center: Rotating words ["Design", "Create", "Inspire"] cycling every 900ms.
AnimatePresence mode="wait"with y:20→0→-20 transitions.text-4xl md:text-6xl lg:text-7xl font-display italic text-text-primary/80. - Bottom-right: Counter display —
text-6xl md:text-8xl lg:text-9xl font-display text-text-primary tabular-nums. ShowsString(count).padStart(3, "0"). - Bottom progress bar:
h-[3px] bg-stroke/50, inner div with.accent-gradient,scaleX(count/100)transform,box-shadow: 0 0 8px rgba(137, 170, 204, 0.35). - On complete (count reaches 100): 400ms delay then calls
onComplete.
Section 2: Hero
Full-viewport section with background HLS video and centered content.
Background Video
- HLS source:
https://stream.mux.com/Aa02T7oM1wH5Mk 5EEVDYhbZ1ChcdhRsS2m1NYyx4Ua1g.m3u8 - Uses
hls.js— ifHls.isSupported(), create HLS instance; else if native HLS support, setvideo.srcdirectly. - Video:
autoPlay muted loop playsInline, absolutely positioned and centered withmin-w-full min-h-full object-cover -translate-x-1/2 -translate-y-1/2. - Dark overlay:
bg-black/20 - Bottom fade:
h-48 bg-gradient-to-t from-bg to-transparent
Navbar (fixed, floats at top center)
fixed top-0 left-0 right-0 z-50 flex justify-center pt-4 md:pt-6 px-4.
Inner pill: inline-flex items-center rounded-full backdrop-blur-md border border-white/10 bg-surface px-2 py-2. Gets shadow-md shadow-black/10 when scrollY > 100.
Contents (left to right):
- Logo: 9×9 circle with accent gradient border (reverses direction on hover). Inner
bg-bgcircle with "JA" infont-display italic text-[13px]. Scales 110% on hover. - Divider:
w-px h-5 bg-stroke mx-1(hidden on mobile) - Nav links: ["Home", "Work", "Resume"] —
text-xs sm:text-sm rounded-full px-3 sm:px-4 py-1.5 sm:py-2. Active:text-text-primary bg-stroke/50. Inactive:text-muted hover:text-text-primary hover:bg-stroke/50. - Divider
- "Say hi" button: Same size as nav links. On hover, shows accent gradient border behind (using absolute span with
inset: -2px). Inner content wrapped inbg-surface rounded-full backdrop-blur-md. Includes "↗" arrow.
Hero Content (centered, z-10)
- Eyebrow:
text-xs text-muted uppercase tracking-[0.3em] mb-8— "COLLECTION '26". Classblur-in. - Name:
text-6xl md:text-8xl lg:text-9xl font-display italic leading-[0.9] tracking-tight text-text-primary mb-6— "Michael Smith". Classname-reveal. - Role line: "A {role} lives in Chicago." — roles cycle every 2s through ["Creative", "Fullstack", "Founder", "Scholar"]. Role word uses
font-display italic text-text-primary animate-role-fade-in inline-blockwithkey={roleIndex}for re-triggering animation. - Description:
text-sm md:text-base text-muted max-w-md mb-12— "Designing seamless digital interactions by focusing on the unique nuances which bring systems to life." - CTA Buttons (inline-flex gap-4):
- "See Works": Solid button. Default:
bg-text-primary text-bg. Hover:bg-bg text-text-primarywith accent gradient border ring. - "Reach out...": Outlined button. Default:
border-2 border-stroke bg-bg text-text-primary. Hover:border-transparentwith accent gradient border ring. - Both:
rounded-full text-sm px-7 py-3.5 hover:scale-105.
- "See Works": Solid button. Default:
GSAP Entrance
Timeline with ease: "power3.out":
.name-reveal: opacity 0→1, y 50→0, duration 1.2s, delay 0.1s.blur-in: opacity 0→1, filter blur(10px)→blur(0px), y 20→0, duration 1s, stagger 0.1, delay 0.3s
Scroll Indicator
Bottom-center, text-xs text-muted uppercase tracking-[0.2em] "SCROLL" label above a w-px h-10 bg-stroke line with animated highlight using .animate-scroll-down.
Section 3: Selected Works
bg-bg py-12 md:py-16. Inner: max-w-[1200px] mx-auto px-6 md:px-10 lg:px-16.
Header
Framer Motion whileInView — opacity 0→1, y 30→0, duration 1s, ease [0.25,0.1,0.25,1], viewport once margin "-100px".
- Eyebrow:
w-8 h-px bg-stroke+ "Selected Work"text-xs text-muted uppercase tracking-[0.3em] - Heading: "Featured projects" — italic word in
font-display italic - Subtext: "A selection of projects I've worked on, from concept to launch."
- "View all work" button (desktop only, hidden md:inline-flex) — rounded-full with gradient hover border ring + right arrow
Bento Grid
grid grid-cols-1 md:grid-cols-12 gap-5 md:gap-6. Column spans alternate: 7/5/5/7.
4 project cards:
[
{ slug: "automotive-motion", title: "Automotive Motion", image: "/projects/wireframe.png", gradient: "from-violet-500 via-fuchsia-400/60 via-indigo-500/60 to-transparent" },
{ slug: "urban-architecture", title: "Urban Architecture", image: "/projects/building.png", gradient: "from-sky-500 via-blue-400/60 to-transparent" },
{ slug: "human-perspective", title: "Human Perspective", image: "/projects/person.png", gradient: "from-emerald-500 via-emerald-300/60 via-teal-500/60 to-transparent" },
{ slug: "brand-identity", title: "Brand Identity", image: "/projects/branding.png", gradient: "from-amber-500 via-amber-300/60 via-orange-500/60 to-transparent" },
]
Each card: bg-surface border border-stroke rounded-3xl with aspect ratios [4/3, 4/3 md:h-full, 4/3 md:h-full, 4/3]. Contains:
- Background image with
object-cover group-hover:scale-105 - Halftone overlay:
radial-gradient(circle, #000 1px, transparent 1px)at 4×4px,opacity-20 mix-blend-multiply - Hover:
bg-bg/70 opacity-0→1+backdrop-blur-lg - Hover label: pill with animated gradient border, white bg, "View — Title" (title in
font-display italic)
Framer Motion: each card staggers with delay: i * 0.1.
Section 4: Journal
bg-bg py-16 md:py-24. Same header pattern (eyebrow + "Recent thoughts" + subtext + "View all" button).
4 journal entries displayed as horizontal pills (rounded-[40px] sm:rounded-full):
[
{ title: "The Future of Generative Art in 2026", image: "/explorations/planet.jpeg", readTime: "6 min read", date: "Feb 13, 2026" },
{ title: "Designing for the Next Billion Users", image: "/explorations/cubes.jpeg", readTime: "5 min read", date: "Feb 06, 2026" },
{ title: "The Psychology of Minimalist Motion", image: "/explorations/ascii.jpeg", readTime: "6 min read", date: "Feb 03, 2026" },
{ title: "The Importance of Mobile-First Design", image: "/explorations/smoke.jpeg", readTime: "5 min read", date: "Jan 31, 2026" },
]
Each entry: flex items-center gap-6 p-4 bg-surface/30 hover:bg-surface border border-stroke. Contains:
- Circular image (100×100px,
rounded-full overflow-hidden, hover:scale-110, border transitions) - Title:
text-lg md:text-2xl font-medium,group-hover:translate-x-1 - Dotted line separator (desktop):
flex-grow h-px bg-stroke/30 - Read time + dot + date
- Arrow circle:
w-10 h-10 rounded-full border border-stroke, hover: fillsbg-text-primary text-bg
Framer Motion: alternating x:-20/+20 entrance per entry.
Section 5: Explorations (Parallax Gallery)
min-h-[300vh] section for scroll-driven parallax.
Layer 1: Pinned Center (z-10)
h-screen div pinned with GSAP ScrollTrigger.create({ pin: contentRef, pinSpacing: false }).
- Eyebrow: "Explorations"
- Heading: "Visual playground"
- Subtext: "A space for creative experiments, motion studies, and visual explorations."
- Dribbble button: pink icon (#ea4c89) + "View on Dribbble" + NE arrow. Gradient hover border ring.
Layer 2: Parallax Columns (z-20, absolute)
grid grid-cols-2 gap-12 md:gap-40 inside max-w-[1400px].
6 items split into 2 columns (even→left, odd→right):
[
{ id: 1, title: "Celestial Planets", category: "3D Visualization", image: "/explorations/planet.jpeg" },
{ id: 2, title: "ASCII Art Study", category: "Generative Art", image: "/explorations/ascii.jpeg" },
{ id: 3, title: "Atmospheric Smoke", category: "Visual Effects", image: "/explorations/smoke.jpeg" },
{ id: 4, title: "Abstract Cylinder", category: "3D Rendering", image: "/explorations/cylinder.jpeg" },
{ id: 5, title: "Organic Waves", category: "Motion Design", image: "/explorations/wave.jpeg" },
{ id: 6, title: "Geometric Cubes", category: "3D Composition", image: "/explorations/cubes.jpeg" },
]
- Left column:
y: "10vh" → "-120vh", scrub 1, spacerh-[20vh] - Right column:
y: "40vh" → "-100vh", scrub 1.5, spacerh-[40vh] - Cards:
aspect-square max-w-[320px], rotation(id%2===0 ? 1 : -1) * (1.5 + id%3)degrees - Each card: outer border frame at
-inset-4 rounded-[40px], blue tintbg-blue-500/10 mix-blend-overlay, halftone texture, hover gradient + content reveal - Clicking opens lightbox:
fixed z-[100] bg-black/95, image ataspect-[16/10], Framer Motion scale 0.9→1, close on backdrop click or Escape key
Section 6: Stats
bg-bg py-16 md:py-24. Same header pattern (eyebrow "Stats & Facts" + "Making an impact" + long subtext).
3-column grid (sm:grid-cols-2 lg:grid-cols-3, 3rd card gets sm:col-span-2 lg:col-span-1):
[
{ number: "20+", label: "Years Experience", sublabel: "In the web design industry field." },
{ number: "95+", label: "Projects Done", sublabel: "Around worldwide in last five years." },
{ number: "200%", label: "Satisfied Clients", sublabel: "With a great experience and results." },
]
Each: large number text-6xl sm:text-7xl md:text-8xl lg:text-9xl font-medium tracking-tighter → h-px bg-stroke divider → label (text-xl md:text-2xl font-bold) + sublabel (text-sm text-muted). Staggered Framer Motion entrance.
Section 7: Contact / Footer
bg-bg pt-16 md:pt-20 pb-8 md:pb-12 overflow-hidden.
Background Video
Same HLS source as hero, but flipped vertically (scale-y-[-1]). Heavier overlay: bg-black/60. Top fade h-32 + bottom fade h-24.
GSAP Marquee
"BUILDING THE FUTURE • " repeated 10×. text-5xl md:text-7xl lg:text-8xl font-display italic text-text-primary/10. GSAP xPercent: -50, duration 40, ease "none", repeat -1.
CTA
- Subtext: "Have a project in mind? I'm always open to new ideas and collaborations."
- Email button:
mailto:[email protected]—px-8 py-4 bg-bg border-2 border-stroke rounded-fullwith gradient hover border ring.whileTap={{ scale: 0.97 }}. NE arrow icon.
Footer Bar
border-t border-stroke pt-8 flex flex-col md:flex-row items-center justify-between.
- Left: Social links [Twitter, LinkedIn, Dribbble, GitHub] —
text-sm text-muted hover:text-text-primary hover:-translate-y-0.5 - Right: Green pulsing dot (
animate-ping bg-green-400+ solidbg-green-500) + "Available for projects"
Required Assets (in /public/)
/projects/wireframe.png,/projects/building.png,/projects/person.png,/projects/branding.png/explorations/planet.jpeg,/explorations/ascii.jpeg,/explorations/smoke.jpeg,/explorations/cylinder.jpeg,/explorations/wave.jpeg,/explorations/cubes.jpeg
Dependencies
gsap, framer-motion, hls.js, react-router-dom, tailwindcss-animate
Add smooth scroll nav Add page transitions