How I built my portfolio with Next.js and Three.js
A deep dive into the tech stack behind lekiks.fr — Next.js 16, Three.js for 3D, Tailwind CSS, and the design decisions that shaped the site.
Why I rebuilt my portfolio (again)
Every developer has that itch. Your portfolio is "fine" but it doesn't feel like you anymore. The tech is outdated, the design is stale, and you've learned so much since the last version that looking at the old code makes you cringe.
So I rebuilt it. From scratch. And I'm pretty happy with the result.
The stack
Here's what powers lekiks.fr:
- Next.js 16 — App Router, server components, static generation
- React 19 — Client components where interactivity is needed
- Three.js — 3D morph blob animation on the hero
- Tailwind CSS 3 — Utility-first styling
- TypeScript — Because life's too short for runtime type errors
- Vercel — Deployment on push to main
No CMS, no database, no external dependencies beyond what's necessary. The goal was speed — both in development and page load.
The 3D blob: Three.js in React
The hero section features an animated 3D blob that morphs and shifts colors. It's built with vanilla Three.js (no React Three Fiber) for maximum control and minimal bundle impact.
The key techniques:
// Morph the sphere geometry using simplex noise
const positions = geometry.attributes.position;
for (let i = 0; i < positions.count; i++) {
const vertex = new THREE.Vector3(
positions.getX(i),
positions.getY(i),
positions.getZ(i)
);
vertex.normalize();
const noise = simplex.noise3D(
vertex.x * frequency + time,
vertex.y * frequency,
vertex.z * frequency
);
vertex.multiplyScalar(radius + noise * amplitude);
positions.setXYZ(i, vertex.x, vertex.y, vertex.z);
}
The blob responds to scroll position — as you scroll down, it subtly changes shape and opacity. This creates a living, breathing feel without being distracting.
Performance considerations
3D on the web is a battery killer if you're not careful. A few things I did:
- Lazy loading: The Three.js canvas only initializes after the splash screen
- RequestAnimationFrame throttling: Animation runs at 30fps, not 60 — the human eye barely notices on organic shapes
- Responsive quality: Lower polygon count on mobile devices
- Cleanup: Proper disposal of geometries, materials, and renderers when the component unmounts
Design philosophy: less is more
The design follows a strict "minimal but warm" philosophy:
- Two fonts: Sora for headings (geometric, modern), DM Sans for body (clean, readable)
- Two colors: Black and white, with gray for hierarchy. No accent colors in the base theme.
- Generous whitespace: Sections breathe with
py-24spacing - Subtle borders:
border-gray-200in light mode,border-gray-800in dark mode — just enough structure without heavy dividers
Dark mode is handled via prefers-color-scheme: media in Tailwind — it respects the OS preference with zero JavaScript.
Scroll animations
Every section uses an IntersectionObserver-based fade-in animation:
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setIsVisible(true)
},
{ threshold: 0.1 }
);
if (sectionRef.current) observer.observe(sectionRef.current);
return () => observer.disconnect();
}, []);
The CSS is dead simple:
.scroll-fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.scroll-fade-in.visible {
opacity: 1;
transform: translateY(0);
}
No animation library needed. The native Intersection Observer API is lightweight and performant.
SEO strategy
For a portfolio site, SEO matters more than you'd think. Potential clients Google you. Recruiters check your web presence. Here's what I implemented:
- Structured data: Schema.org Person, WebSite, and now BlogPosting types
- Open Graph & Twitter cards: Every page has proper social preview metadata
- Sitemap: Auto-generated via Next.js
sitemap.ts - Semantic HTML: Proper heading hierarchy, landmark elements, alt text
- Performance: 95+ Lighthouse scores across the board
What I'd do differently
If I rebuilt this tomorrow:
- Use React Three Fiber — The abstraction has gotten really good, and it would simplify the Three.js code significantly
- Add page transitions — The abrupt route changes between the homepage and blog feel jarring
- Implement a CMS — Writing MDX files works, but a headless CMS would make publishing easier
But honestly? It works. It's fast. It represents who I am. And that's the whole point of a portfolio.
The full source code isn't public (yet), but I share technical breakdowns like this on X. Follow along if you're into this stuff.