Skip to content

Solution: Portfolio v2 with Animations

ลองเล่น Portfolio v2 ที่สมบูรณ์แล้ว — ลอง scroll ดู animation, hover ที่ project cards, และคลิก nav links ดู

Portfolio.jsx
Complete Demo INTERACTIVE
Live preview Scroll inside the frame

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

Alex D. UX/UI Engineer

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.

React 90%
Tailwind CSS 85%
Framer Motion 80%
Figma 85%
TypeScript 75%
Node.js 70%

My Projects

Get In Touch

Have a project in mind or want to collaborate?
Feel free to reach out — I'd love to hear from you.

  • 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
Show variants.js
src/animations/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 children
export const staggerContainer = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.1,
},
},
};
// Scale up เมื่อ hover
export const scaleOnHover = {
whileHover: { scale: 1.03, y: -4 },
whileTap: { scale: 0.98 },
transition: { type: "spring", stiffness: 300, damping: 20 },
};
// Navbar animation
export const navVariants = {
hidden: { opacity: 0, y: -20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: "easeOut" },
},
};
Show Navbar.jsx
src/components/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
src/components/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
src/components/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
src/components/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
src/components/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"
>
&copy; {new Date().getFullYear()} Portfolio. Built with React, Tailwind
CSS & Framer Motion.
</motion.p>
</div>
</section>
);
}
Show App.jsx
src/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>
);
}
  1. variants.js — Shared Animation Config

    แยก animation variants ออกเป็นไฟล์กลาง ทำให้ทุก component ใช้ animation style เดียวกัน และแก้ไขได้จากที่เดียว fadeInUp + staggerContainer คือ combo ที่ใช้บ่อยที่สุด

    Preview Output
    A
    fadeInUp
    B
    fadeIn
    C
    slideInLeft
    D
    scaleOnHover
  2. Navbar.jsx — Scroll-aware Navigation

    ใช้ useState + useEffect ฟัง scroll event เพื่อเปลี่ยน background เมื่อ scroll ลง ใช้ backdrop-blur-md ทำ glass effect และมี mobile menu ที่ animate เข้า-ออก

    Preview Output
    scroll ↓
    transparent (top) solid bg (scrolled)
  3. Hero.jsx — Delay Chaining

    แต่ละ element มี delay ต่างกัน (0.2, 0.4, 0.6, 0.8) ทำให้เข้ามาตามลำดับ ใช้ bg-gradient-to-br สร้าง background decoration แบบ subtle

    Preview Output
    delay: 0.2s Hello, I'm
    delay: 0.4s UX/UI Engineer
    delay: 0.6s I design and build beautiful experiences...
    delay: 0.8s
  4. About.jsx — Scroll + Stagger

    ใช้ whileInView ให้ animate เมื่อ scroll มาถึง viewport={{ once: true }} ทำให้ animate ครั้งเดียว Skill bars ใช้ initial={{ width: 0 }} แล้ว animate ไปตาม percentage

    Preview Output
    About Me
    I'm a UX/UI Engineer who loves crafting...
    whileInView={ opacity: 1, y: 0 }
    Design
    90%
    React
    80%
    CSS
    95%
    Motion
    75%
  5. Projects.jsx — Card Grid + Hover

    ใช้ staggerContainer ทำให้ cards เข้ามาทีละตัว แต่ละ card มี whileHover ที่ยกขึ้น + เพิ่มเงา รูปภาพมี zoom effect เมื่อ hover

    Preview Output
    E-Commerce Dashboard
    ReactTailwind
    Weather App
    ReactAPI
    Task Manager
    ReactDnD
    staggerChildren: 0.15 + whileHover: translateY(-4px)
  6. Contact.jsx — Form + Social Links

    Form fields ใช้ stagger animation เข้ามาทีละ field Social links มี whileHover ที่ขยาย + ยกขึ้น Footer fade in เป็นอันสุดท้าย

    Preview Output
    Get In Touch
    Your Name
    your@email.com
    Message...
    Send Message
    GitHub LinkedIn Dribbble
  7. App.jsx — รวมทุกอย่าง

    ScrollProgress ใช้ useScroll + scaleX ทำ progress bar ที่ fixed อยู่บนสุด ทุก component เรียงตามลำดับ section bg-gray-950 กับ bg-gray-900 สลับกันสร้าง visual separation

  • ใช้ 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 ได้จริง