Sohaib
← All posts

Blog · December 20, 2025 · 5 min read

Building My Portfolio with Next.js 16, Tailwind v4 & Framer Motion

A behind-the-scenes look at the design decisions, performance optimisations, and animation techniques used to build this very site.

#Next.js#React#Tailwind CSS#Web Dev

Why Next.js 16?

Next.js 16 ships with React 19 and the App Router as the default. The file-system routing, built-in metadata API, and server components make it ideal for a content-heavy portfolio.


Tailwind v4, A Different Mental Model

Tailwind v4 drops the tailwind.config.js file. Configuration moves into globals.css using @theme:

@import "tailwindcss";

@theme inline {
  --color-accent-indigo: #6366f1;
  --color-accent-cyan: #06b6d4;
  --font-display: "Space Grotesk", sans-serif;
}

These become CSS variables and Tailwind utilities automatically. So bg-accent-indigo just works.


Framer Motion Patterns

Reveal on Scroll

"use client";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";

export function Reveal({ children }: { children: React.ReactNode }) {
  const ref = useRef(null);
  const inView = useInView(ref, { once: true, margin: "-80px" });

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 24 }}
      animate={inView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
    >
      {children}
    </motion.div>
  );
}

Staggered Children

const container = {
  hidden: {},
  show: {
    transition: { staggerChildren: 0.08 }
  }
};

const item = {
  hidden: { opacity: 0, y: 20 },
  show:   { opacity: 1, y: 0, transition: { duration: 0.5 } }
};

<motion.ul variants={container} initial="hidden" animate="show">
  {items.map(i => (
    <motion.li key={i} variants={item}>{i}</motion.li>
  ))}
</motion.ul>

Performance Notes

  • All heavy components use "use client" only where needed, server components handle static markup.
  • Images served from /public, no external image domains needed.
  • Font loading via next/font/google with display: swap to avoid layout shift.

What I'd Do Differently

  1. MDX for blog posts, richer than plain markdown, composable React components inline.
  2. View Transitions API, browser-native page transitions instead of Framer Motion for route changes.
  3. Incremental Static Regeneration, for blog posts that update infrequently.

The full source is on GitHub.

Sohaib Shamsi

I build the systems money moves through.