Back to blog
nextjsfirebaseblogreact

How I Built This Portfolio With Next.js and Firebase

A technical walkthrough of building a cinematic developer portfolio with Next.js 16, Tailwind CSS v4, Framer Motion animations, MDX blog support, and Firebase Hosting deployment.

TS
Tharun Sai Putta
November 15, 2024
4 min read

title: "How I Built This Portfolio With Next.js and Firebase" date: "2024-11-15" description: "A technical walkthrough of building a cinematic developer portfolio with Next.js 16, Tailwind CSS v4, Framer Motion animations, MDX blog support, and Firebase Hosting deployment." tags: ["nextjs", "firebase", "blog", "react"] published: true image: ""

I wanted a portfolio that didn't look like every other developer's site. No stock templates, no boring grids. Something that felt like me — dark, animated, technically interesting to look at.

Here's how I built it.

Tech stack decisions

The core choices:

LayerChoiceWhy
FrameworkNext.js 16App Router, static export, MDX support
StylingTailwind CSS v4CSS variables + utility classes, no config file
AnimationFramer MotionScroll-triggered reveals, spring physics
Blog@next/mdx + ShikiBuild-time compilation, zero client JS for syntax highlighting
DeployFirebase HostingFree tier, global CDN, fast

The cinematic design system

The visual identity is built around a dark color palette with a green accent:

:root {
  --bg-primary: #0a0a14;
  --bg-surface: #0f0f1a;
  --accent: #00e8a0;
  --accent-blue: #3b82f6;
  --text-primary: #ffffff;
  --text-body: rgba(255, 255, 255, 0.7);
}

Tailwind v4 (the new version without a config file) lets you register these as utility classes using the @theme block:

@theme inline {
  --color-accent: var(--accent);
  --color-bg-primary: var(--bg-primary);
  /* now you can use bg-bg-primary, text-accent, etc. */
}

This means theme switching is a single data attribute on <html>:

// ThemeProvider.tsx
<NextThemesProvider attribute="data-theme" defaultTheme="dark">
  {children}
</NextThemesProvider>

Floating shapes and glows

The hero section background has 16+ floating geometric shapes, each with its own CSS animation:

@keyframes floatRing {
  0%, 100% { transform: translateY(0px) rotate(0deg); }
  50% { transform: translateY(-18px) rotate(8deg); }
}

Each <FloatingShape> component just renders a CSS shape (ring, dot, cross, triangle, diamond, hexagon) with will-change: transform for GPU compositing and a unique animation-delay so they don't all move in sync.

The glow orbs are blurred radial gradients — visually impactful, zero JavaScript:

<div style={{
  background: "#00e8a0",
  filter: "blur(80px)",
  borderRadius: "50%",
  opacity: 0.12,
  animation: "glowPulse 8s ease-in-out infinite",
}} />

MDX blog with build-time syntax highlighting

The blog uses @next/mdx for compilation. Posts live in content/posts/*.mdx and are compiled at build time by webpack.

Syntax highlighting is handled by rehype-pretty-code + Shiki. The highlight is added as inline styles at build time — no client-side JS required:

// next.config.ts
const withMDX = createMDX({
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [[rehypePrettyCode, { theme: "one-dark-pro" }]],
  },
});

For the blog listing page, I use gray-matter + Node's fs module to read frontmatter at build time:

export function getAllPosts(): Post[] {
  const slugs = getPostSlugs();
  return slugs
    .map((slug) => getPostBySlug(slug))
    .filter((p): p is Post => p !== null && p.published)
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}

Important: fs only runs server-side at build time. Never import this into a client component.

Scroll animations with Framer Motion

Cards and sections reveal as you scroll using useInView:

export default function SectionReveal({ children, delay = 0 }) {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: "-80px" });
 
  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 30 }}
      animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
      transition={{ duration: 0.6, delay, ease: [0.16, 1, 0.3, 1] }}
    >
      {children}
    </motion.div>
  );
}

The [0.16, 1, 0.3, 1] cubic bezier is an "overshoot and settle" curve that feels snappy without being jarring.

Static export + Firebase

Since Firebase Hosting serves static files, the site exports to HTML:

// next.config.ts
const nextConfig = {
  output: "export",
  images: { unoptimized: true },
};

The GitHub Actions CI/CD pipeline handles deployment on every push to main:

- name: Deploy to Firebase
  uses: FirebaseExtended/action-hosting-deploy@v0
  with:
    repoToken: ${{ secrets.GITHUB_TOKEN }}
    firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}

What I'd do differently

Honestly, not much. The main friction point was Tailwind v4 — the docs are still catching up and the @theme syntax for CSS variable registration took some trial and error. If I were doing this again, I'd read the Tailwind v4 alpha docs more carefully first.

The MDX setup was surprisingly smooth once I committed to @next/mdx over next-mdx-remote — the official integration is cleaner and has better TypeScript support.


The site is open source — you can poke around the code and steal whatever's useful.

TS

Tharun Sai Putta

Product Engineer @ Protectt.ai

Building Android security SDKs, IDE plugins, and cross-platform tooling. IIITDM Kancheepuram CSE alumnus.