Solution: Portfolio v2 with Animations
Try the Complete App
Section titled “Try the Complete App”ลองเล่น Portfolio v2 ที่สมบูรณ์แล้ว — ลอง scroll ดู animation, hover ที่ project cards, และคลิก nav links ดู
Welcome to my portfolio
Hi, I'm Alex
UX/UI Engineer
I design and build beautiful, performant web experiences with attention to detail and motion.
About Me
I'm a UX/UI Engineer who loves crafting beautiful digital experiences. I combine design thinking with technical skills to build products that look great and feel natural to use.
With a focus on animation and micro-interactions, I bring interfaces to life — making every hover, scroll, and transition feel intentional and delightful.
My Projects
E-Commerce Dashboard
A modern dashboard for managing online store inventory, orders, and analytics.
Open case studyWeather App
Beautiful weather app with location search, forecasts, and animated icons.
Open case studyTask Manager
Drag-and-drop task management with categories and progress tracking.
Open case studyPortfolio v1
My first portfolio website built with HTML, CSS, and vanilla JavaScript. Fully responsive design.
Open case studyGet In Touch
Have a project in mind or want to collaborate?
Feel free to reach out — I'd love to hear from you.
Final Project Structure
Section titled “Final Project Structure”Directoryportfolio-v2/
Directorysrc/
- App.jsx Main app — รวมทุก component + ScrollProgress
Directorycomponents/
- Navbar.jsx Navigation พร้อม scroll background change
- Hero.jsx Hero section พร้อม delay chaining animation
- About.jsx About section พร้อม scroll + stagger animation
- Projects.jsx Projects grid พร้อม hover + stagger
- Contact.jsx Contact section พร้อม animation
Directoryanimations/
- variants.js Shared animation variants
- package.json Dependencies
Solution Code
Section titled “Solution Code”Show variants.js
// Fade in + slide up — ใช้บ่อยที่สุดexport const fadeInUp = { hidden: { opacity: 0, y: 30 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: "easeOut" }, },};
// Fade in อย่างเดียวexport const fadeIn = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 0.6 }, },};
// Slide in จากซ้ายexport const slideInLeft = { hidden: { opacity: 0, x: -60 }, visible: { opacity: 1, x: 0, transition: { duration: 0.6, ease: "easeOut" }, },};
// Slide in จากขวาexport const slideInRight = { hidden: { opacity: 0, x: 60 }, visible: { opacity: 1, x: 0, transition: { duration: 0.6, ease: "easeOut" }, },};
// Container ที่ stagger childrenexport const staggerContainer = { hidden: {}, visible: { transition: { staggerChildren: 0.1, delayChildren: 0.1, }, },};
// Scale up เมื่อ hoverexport const scaleOnHover = { whileHover: { scale: 1.03, y: -4 }, whileTap: { scale: 0.98 }, transition: { type: "spring", stiffness: 300, damping: 20 },};
// Navbar animationexport const navVariants = { hidden: { opacity: 0, y: -20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" }, },};Show Navbar.jsx
import { useState, useEffect } from "react";import { motion } from "motion/react";import { navVariants } from "../animations/variants";
const navLinks = [ { label: "About", href: "#about" }, { label: "Projects", href: "#projects" }, { label: "Contact", href: "#contact" },];
export default function Navbar() { const [scrolled, setScrolled] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => { const handleScroll = () => setScrolled(window.scrollY > 50); window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, []);
const handleClick = (e, href) => { e.preventDefault(); setMobileOpen(false); document.querySelector(href)?.scrollIntoView({ behavior: "smooth" }); };
return ( <motion.nav variants={navVariants} initial="hidden" animate="visible" className={`fixed top-0 left-0 right-0 z-50 transition-colors duration-300 ${ scrolled ? "bg-gray-900/95 backdrop-blur-md shadow-lg" : "bg-transparent" }`} > <div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between"> <motion.a href="#" className="text-xl font-bold text-white" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > Portfolio </motion.a>
{/* Desktop Links */} <ul className="hidden md:flex items-center gap-8"> {navLinks.map((link) => ( <li key={link.label}> <motion.a href={link.href} onClick={(e) => handleClick(e, link.href)} className="text-gray-300 hover:text-white transition-colors text-sm font-medium" whileHover={{ y: -2 }} transition={{ type: "spring", stiffness: 300 }} > {link.label} </motion.a> </li> ))} </ul>
{/* Mobile Toggle */} <button onClick={() => setMobileOpen(!mobileOpen)} className="md:hidden text-white p-2" aria-label="Toggle menu" > <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} > {mobileOpen ? ( <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> ) : ( <path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" /> )} </svg> </button> </div>
{/* Mobile Menu */} {mobileOpen && ( <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="md:hidden bg-gray-900/95 backdrop-blur-md border-t border-gray-800" > <ul className="flex flex-col px-6 py-4 gap-4"> {navLinks.map((link) => ( <li key={link.label}> <a href={link.href} onClick={(e) => handleClick(e, link.href)} className="text-gray-300 hover:text-white transition-colors text-sm font-medium block" > {link.label} </a> </li> ))} </ul> </motion.div> )} </motion.nav> );}Show Hero.jsx
import { motion } from "motion/react";
export default function Hero() { const handleScrollToProjects = () => { document.querySelector("#projects")?.scrollIntoView({ behavior: "smooth" }); };
return ( <section className="min-h-screen flex items-center justify-center bg-gray-950 relative overflow-hidden"> {/* Background gradient decoration */} <div className="absolute inset-0 bg-gradient-to-br from-blue-600/10 via-transparent to-purple-600/10" />
<div className="text-center px-6 relative z-10"> <motion.p initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2, duration: 0.5 }} className="text-sm md:text-base text-blue-400 font-medium mb-4 tracking-wide uppercase" > Welcome to my portfolio </motion.p>
<motion.h1 initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.4, duration: 0.8, ease: "easeOut" }} className="text-4xl md:text-6xl lg:text-7xl font-bold text-white mb-6 leading-tight" > Hi, I'm{" "} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400"> Alex </span> <br /> <span className="text-gray-400 text-2xl md:text-4xl lg:text-5xl font-medium"> UX/UI Engineer </span> </motion.h1>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.6, duration: 0.6 }} className="text-gray-400 mb-10 max-w-lg mx-auto text-base md:text-lg" > I design and build beautiful, performant web experiences with attention to detail and motion. </motion.p>
<motion.div initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.8, type: "spring", stiffness: 200 }} className="flex flex-col sm:flex-row gap-4 justify-center" > <motion.button onClick={handleScrollToProjects} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} className="px-8 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-medium transition-colors" > View My Work </motion.button> <motion.a href="#contact" onClick={(e) => { e.preventDefault(); document.querySelector("#contact")?.scrollIntoView({ behavior: "smooth" }); }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} className="px-8 py-3 border border-gray-700 hover:border-gray-500 text-gray-300 rounded-xl font-medium transition-colors text-center" > Contact Me </motion.a> </motion.div> </div> </section> );}Show About.jsx
import { motion } from "motion/react";import { fadeInUp, staggerContainer, slideInLeft, slideInRight } from "../animations/variants";
const skills = [ { name: "React", level: 90 }, { name: "Tailwind CSS", level: 85 }, { name: "Framer Motion", level: 80 }, { name: "Figma", level: 85 }, { name: "TypeScript", level: 75 }, { name: "Node.js", level: 70 },];
export default function About() { return ( <section id="about" className="py-20 md:py-32 bg-gray-900"> <div className="max-w-6xl mx-auto px-6"> <motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={{ once: true }} className="text-3xl md:text-4xl font-bold text-white mb-16 text-center" > About Me </motion.h2>
<div className="grid md:grid-cols-2 gap-12 md:gap-16 items-start"> {/* Left: Bio */} <motion.div variants={slideInLeft} initial="hidden" whileInView="visible" viewport={{ once: true }} > <h3 className="text-xl font-semibold text-white mb-4"> Designer & Developer </h3> <p className="text-gray-400 mb-4 leading-relaxed"> I'm a UX/UI Engineer who loves crafting beautiful digital experiences. I combine design thinking with technical skills to build products that look great and feel natural to use. </p> <p className="text-gray-400 leading-relaxed"> With a focus on animation and micro-interactions, I bring interfaces to life — making every hover, scroll, and transition feel intentional and delightful. </p> </motion.div>
{/* Right: Skills */} <motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={{ once: true }} > <h3 className="text-xl font-semibold text-white mb-6">Skills</h3> <div className="space-y-4"> {skills.map((skill) => ( <motion.div key={skill.name} variants={fadeInUp}> <div className="flex justify-between mb-1"> <span className="text-gray-300 text-sm font-medium"> {skill.name} </span> <span className="text-gray-500 text-sm">{skill.level}%</span> </div> <div className="w-full bg-gray-800 rounded-full h-2"> <motion.div className="bg-gradient-to-r from-blue-500 to-purple-500 h-2 rounded-full" initial={{ width: 0 }} whileInView={{ width: `${skill.level}%` }} viewport={{ once: true }} transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }} /> </div> </motion.div> ))} </div> </motion.div> </div> </div> </section> );}Show Projects.jsx
import { motion } from "motion/react";import { fadeInUp, staggerContainer } from "../animations/variants";
const projects = [ { title: "E-Commerce Dashboard", description: "A modern dashboard for managing online store inventory, orders, and analytics with real-time data visualization.", tags: ["React", "Tailwind", "Chart.js"], link: "#", image: "https://placehold.co/600x400/1e293b/64748b?text=Dashboard", }, { title: "Weather App", description: "Beautiful weather application with location search, 7-day forecast, and animated weather icons.", tags: ["React", "API", "Motion"], link: "#", image: "https://placehold.co/600x400/1e293b/64748b?text=Weather", }, { title: "Task Manager", description: "Drag-and-drop task management app with categories, due dates, and progress tracking.", tags: ["React", "DnD", "Tailwind"], link: "#", image: "https://placehold.co/600x400/1e293b/64748b?text=Tasks", }, { title: "Portfolio v1", description: "My first portfolio website built with HTML, CSS, and vanilla JavaScript. Fully responsive design.", tags: ["HTML", "CSS", "JavaScript"], link: "#", image: "https://placehold.co/600x400/1e293b/64748b?text=Portfolio", },];
export default function Projects() { return ( <section id="projects" className="py-20 md:py-32 bg-gray-950"> <div className="max-w-6xl mx-auto px-6"> <motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={{ once: true }} className="text-3xl md:text-4xl font-bold text-white mb-4 text-center" > My Projects </motion.h2>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={{ once: true }} className="text-gray-400 text-center mb-16 max-w-md mx-auto" > A selection of projects I've worked on recently </motion.p>
<motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={{ once: true, margin: "-50px" }} className="grid md:grid-cols-2 gap-6" > {projects.map((project) => ( <motion.a key={project.title} href={project.link} variants={fadeInUp} whileHover={{ y: -6, boxShadow: "0 25px 50px rgba(0,0,0,0.4)" }} transition={{ type: "spring", stiffness: 300, damping: 20 }} className="group block bg-gray-900 rounded-2xl overflow-hidden border border-gray-800 hover:border-gray-700 transition-colors" > {/* Project Image */} <div className="overflow-hidden"> <motion.img src={project.image} alt={project.title} className="w-full h-48 object-cover" whileHover={{ scale: 1.05 }} transition={{ duration: 0.4 }} /> </div>
{/* Project Info */} <div className="p-6"> <h3 className="text-lg font-semibold text-white mb-2 group-hover:text-blue-400 transition-colors"> {project.title} </h3> <p className="text-gray-400 text-sm mb-4 leading-relaxed"> {project.description} </p> <div className="flex flex-wrap gap-2"> {project.tags.map((tag) => ( <span key={tag} className="px-3 py-1 text-xs font-medium bg-gray-800 text-gray-300 rounded-full" > {tag} </span> ))} </div> </div> </motion.a> ))} </motion.div> </div> </section> );}Show Contact.jsx
import { motion } from "motion/react";import { fadeInUp, staggerContainer } from "../animations/variants";
const socialLinks = [ { label: "GitHub", href: "https://github.com", icon: "GH" }, { label: "LinkedIn", href: "https://linkedin.com", icon: "LI" }, { label: "Email", href: "mailto:hello@example.com", icon: "@" },];
export default function Contact() { return ( <section id="contact" className="py-20 md:py-32 bg-gray-900"> <div className="max-w-3xl mx-auto px-6 text-center"> <motion.h2 variants={fadeInUp} initial="hidden" whileInView="visible" viewport={{ once: true }} className="text-3xl md:text-4xl font-bold text-white mb-4" > Get In Touch </motion.h2>
<motion.p variants={fadeInUp} initial="hidden" whileInView="visible" viewport={{ once: true }} className="text-gray-400 mb-12 max-w-md mx-auto" > Have a project in mind or want to collaborate? Feel free to reach out — I'd love to hear from you. </motion.p>
{/* Contact Form */} <motion.form variants={staggerContainer} initial="hidden" whileInView="visible" viewport={{ once: true }} className="space-y-4 mb-16 text-left" onSubmit={(e) => e.preventDefault()} > <div className="grid sm:grid-cols-2 gap-4"> <motion.div variants={fadeInUp}> <label htmlFor="name" className="block text-sm text-gray-400 mb-1"> Name </label> <input type="text" id="name" placeholder="Your name" className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors" /> </motion.div> <motion.div variants={fadeInUp}> <label htmlFor="email" className="block text-sm text-gray-400 mb-1"> Email </label> <input type="email" id="email" placeholder="your@email.com" className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors" /> </motion.div> </div>
<motion.div variants={fadeInUp}> <label htmlFor="message" className="block text-sm text-gray-400 mb-1"> Message </label> <textarea id="message" rows={5} placeholder="Tell me about your project..." className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors resize-none" /> </motion.div>
<motion.div variants={fadeInUp}> <motion.button type="submit" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} className="w-full sm:w-auto px-8 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-medium transition-colors" > Send Message </motion.button> </motion.div> </motion.form>
{/* Social Links */} <motion.div variants={staggerContainer} initial="hidden" whileInView="visible" viewport={{ once: true }} className="flex justify-center gap-4 mb-12" > {socialLinks.map((link) => ( <motion.a key={link.label} href={link.href} target="_blank" rel="noopener noreferrer" variants={fadeInUp} whileHover={{ scale: 1.1, y: -2 }} whileTap={{ scale: 0.95 }} className="w-12 h-12 flex items-center justify-center bg-gray-800 hover:bg-gray-700 border border-gray-700 hover:border-gray-600 rounded-xl text-gray-300 hover:text-white text-sm font-bold transition-colors" aria-label={link.label} > {link.icon} </motion.a> ))} </motion.div>
{/* Footer */} <motion.p initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }} transition={{ delay: 0.3 }} className="text-gray-600 text-sm" > © {new Date().getFullYear()} Portfolio. Built with React, Tailwind CSS & Framer Motion. </motion.p> </div> </section> );}Show App.jsx
import { motion, useScroll } from "motion/react";import Navbar from "./components/Navbar";import Hero from "./components/Hero";import About from "./components/About";import Projects from "./components/Projects";import Contact from "./components/Contact";
function ScrollProgress() { const { scrollYProgress } = useScroll();
return ( <motion.div style={{ scaleX: scrollYProgress }} className="fixed top-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 to-purple-500 origin-left z-[60]" /> );}
export default function App() { return ( <div className="bg-gray-950 text-white"> <ScrollProgress /> <Navbar /> <Hero /> <About /> <Projects /> <Contact /> </div> );}Code Explanation
Section titled “Code Explanation”-
variants.js — Shared Animation Config
แยก animation variants ออกเป็นไฟล์กลาง ทำให้ทุก component ใช้ animation style เดียวกัน และแก้ไขได้จากที่เดียว
fadeInUp+staggerContainerคือ combo ที่ใช้บ่อยที่สุดPreview OutputAfadeInUpBfadeInslideInLeftDscaleOnHover -
Navbar.jsx — Scroll-aware Navigation
ใช้
useState+useEffectฟัง scroll event เพื่อเปลี่ยน background เมื่อ scroll ลง ใช้backdrop-blur-mdทำ glass effect และมี mobile menu ที่ animate เข้า-ออกPreview Output -
Hero.jsx — Delay Chaining
แต่ละ element มี
delayต่างกัน (0.2, 0.4, 0.6, 0.8) ทำให้เข้ามาตามลำดับ ใช้bg-gradient-to-brสร้าง background decoration แบบ subtlePreview Outputdelay: 0.2s Hello, I'mdelay: 0.4s UX/UI Engineerdelay: 0.6s I design and build beautiful experiences...delay: 0.8s -
About.jsx — Scroll + Stagger
ใช้
whileInViewให้ animate เมื่อ scroll มาถึงviewport={{ once: true }}ทำให้ animate ครั้งเดียว Skill bars ใช้initial={{ width: 0 }}แล้ว animate ไปตาม percentagePreview OutputAbout MeI'm a UX/UI Engineer who loves crafting...whileInView={ opacity: 1, y: 0 }Design90%React80%CSS95%Motion75% -
Projects.jsx — Card Grid + Hover
ใช้
staggerContainerทำให้ cards เข้ามาทีละตัว แต่ละ card มีwhileHoverที่ยกขึ้น + เพิ่มเงา รูปภาพมี zoom effect เมื่อ hoverPreview OutputE-Commerce DashboardWeather AppTask ManagerstaggerChildren: 0.15 + whileHover: translateY(-4px) -
Contact.jsx — Form + Social Links
Form fields ใช้ stagger animation เข้ามาทีละ field Social links มี
whileHoverที่ขยาย + ยกขึ้น Footer fade in เป็นอันสุดท้ายPreview OutputGet In TouchYour Nameyour@email.comMessage...Send Message -
App.jsx — รวมทุกอย่าง
ScrollProgressใช้useScroll+scaleXทำ progress bar ที่ fixed อยู่บนสุด ทุก component เรียงตามลำดับ sectionbg-gray-950กับbg-gray-900สลับกันสร้าง visual separation
What You Learned
Section titled “What You Learned”- ใช้ Framer Motion สร้าง animation ใน React ได้
- เข้าใจ initial, animate, transition — 3 props หลักของ motion
- ใช้ whileHover, whileTap สร้าง interactive animation
- ใช้ whileInView ทำ scroll-triggered animation
- ใช้ variants + staggerChildren จัดการ animation หลายตัว
- ใช้ useScroll สร้าง scroll progress bar
- แยก variants เป็นไฟล์กลาง เพื่อ reuse ข้าม component
- Deploy เว็บขึ้น Vercel หรือ Netlify ได้จริง