← All Posts

Redesigning My Portfolio with Nitro-Inspired UI

5 min readMarch 12, 2026
designnextjsframer-motioncsstailwindclaude-codeportfolio

Redesigning My Portfolio with Nitro-Inspired UI

Hero
Hero

I've been shipping projects and writing blog posts for months, but the site itself was starting to feel... generic. Clean, sure. Functional, yes. But it didn't have personality. Then I came across the Nitro Framer template and everything clicked.

This post documents how I redesigned my entire portfolio in a single session — the design decisions, the CSS tricks, the animation math, and the iterative debugging that turned a standard dark-mode portfolio into something with real visual identity.

The Inspiration

Nitro nails a few things that most portfolio templates get wrong:

  • Scroll-stacking project cards — cards stack on top of each other like a deck as you scroll, each with a bold color theme
  • Cinematic restraint — dark, moody, with film grain and subtle blur effects
  • Typography as personality — uppercase titles, mono metadata, generous whitespace
  • Minimal chrome — transparent navbar, no gradient buttons, no decorative noise

I didn't want to clone Nitro. I wanted to extract the principles and apply them to my existing Next.js + Tailwind stack.

Before & After

The transformation speaks for itself:

Before — gradient hero, standard card layout, light/dark hybrid:

Before homepage
Before homepage

After — dark, moody, cinematic, with scroll-stacking cards:

After homepage
After homepage

The Scroll-Stacking Cards

This was the centerpiece. In Nitro, project cards stack on top of each other as you scroll — the current card stays sticky while the next one slides up from below, creating a deck-of-cards effect.

Scroll stacking cards mid-transition
Scroll stacking cards mid-transition

The Math

Getting the timing right was the hardest part. Three variables control the feel:

container height = how long each card stays visible
negative margin = how much containers overlap
solo time = height + margin = how long before next card appears

I went through several iterations:

HeightMarginSolo TimeResult
150vh-55vh95vhToo much gap — dead space between cards
115vh-75vh40vhToo tight — cards covered before fully visible
130vh-60vh70vhStill too much gap
160vh-85vh75vhJust right — full card visible, tight transitions

The Animation

Each card uses Framer Motion's useScroll and useTransform to track scroll progress:

const scale = useTransform(
  scrollYProgress,
  [0, 0.2, 0.45],
  isLast ? [1, 1, 1] : [1, 1, 0.85]
);
const contentOpacity = useTransform(
  scrollYProgress,
  [0, 0.15, 0.4],
  isLast ? [1, 1, 1] : [1, 1, 0.15]
);

The key insight: previous cards fade to 0.15 opacity (not zero) and scale to 0.85 — they become ghost strips at the top, creating the stacked deck visual. Each card gets a 14px sticky offset so the strips are visible but compact.

Coordinated Color Themes

Each card gets a coordinated color palette — background, text, metadata, divider, and arrow colors that all match:

Project cards with color themes
Project cards with color themes

const themes = [
  { bg: "#F97316", text: "#1A1A1A", meta: "rgba(0,0,0,0.45)" },
  { bg: "#EFEFFF", text: "#4338CA", meta: "#4338CA" },
  { bg: "#1E1E1E", text: "#86EFAC", meta: "rgba(134,239,172,0.5)" },
  { bg: "#FACC15", text: "#1A1A1A", meta: "rgba(0,0,0,0.45)" },
];

Cinematic Portrait Treatment

Nitro uses film grain overlays on images. I applied this to my about section portrait with a layered approach:

About section with film grain
About section with film grain

  1. Dim + desaturate the base image: brightness-75 saturate-[0.6]
  2. Gradient overlays on all four edges for a vignette effect
  3. Animated film grain using a noise PNG texture:
@keyframes grain {
  0%, 100% { transform: translate(0, 0); }
  10% { transform: translate(-5%, -10%); }
  30% { transform: translate(7%, -25%); }
}

.grain-overlay::after {
  background-image: url("/images/noise.png");
  opacity: 0.2;
  mix-blend-mode: overlay;
  animation: grain 4s steps(8) infinite;
}

The steps(8) timing function is crucial — it creates a film-like stutter rather than smooth interpolation.

Transparent Navbar

A small change with big impact. The navbar went from solid black to:

background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);

Now the hero gradient and content bleeds through the nav, creating depth. The subtle border provides structure without heaviness.

Blog Page: Editorial Numbers & Image Fades

Each blog post gets a large editorial number — a gradient-filled numeral that fades from indigo to transparent:

Blog page with editorial numbers
Blog page with editorial numbers

.post-number {
  font-size: 2.5rem; /* scales to 6rem on desktop */
  font-weight: 800;
  background: linear-gradient(180deg, rgba(99,102,241,0.25) 0%, transparent 80%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

The blog thumbnails use CSS mask-image to fade into the card:

mask-image: linear-gradient(to right, transparent 0%, black 40%);

This creates a seamless blend between the text content and the hero image — no hard edges.

Projects Grid

The projects page got upgraded too — the hero card now uses real project screenshots with gradient overlays instead of emoji placeholders:

Projects page grid
Projects page grid

Vertical Grid Lines

A subtle but effective touch — six vertical lines at 4% opacity span the entire page:

<div className="fixed inset-0 z-0 pointer-events-none">
  <div className="mx-8 md:mx-16 lg:mx-24 h-full flex justify-between">
    {[...Array(6)].map((_, i) => (
      <div key={i} className="w-px h-full bg-white/[0.04]" />
    ))}
  </div>
</div>

They match the page's horizontal padding, creating an invisible grid that makes everything feel intentionally placed.

AI-Generated Project Images

Some projects don't have a live UI to screenshot (like the TPM Agent Ecosystem). For these, I used two approaches:

  1. HTML diagrams — render an architecture diagram as styled HTML, then screenshot with agent-browser
  2. AI generation — use Gemini 2.5 Flash Image to generate hero images

Key lesson: avoid text in AI image prompts. My first LuxeVintage hero image had garbled text on every UI element. The fix was prompting for "no text anywhere" and letting the visual composition speak for itself.

Redesigned Footer

The footer went from a gradient-heavy design to a minimal mono aesthetic:

Redesigned footer
Redesigned footer

Mono font, subtle colors, matching the page's horizontal padding. No icons, no gradients — just information.

Mobile Debugging

Two issues caught me after the initial deploy:

  1. Portrait cutoff — the two-column grid stacks on mobile, but min-h-[280px] was too short and bg-top showed the top of my head instead of my face. Fix: min-h-[400px] + bg-[center_20%]
  2. Editorial numbers clipped4rem numbers in a w-10 column overflowed. Fix: responsive widths w-12 sm:w-20 md:w-24 with 2.5rem base scaling to 6rem

What I Learned

  1. Reference designs are gold — having Nitro as a north star made every decision faster
  2. The math matters — scroll-stacking required precise tuning of container heights, overlaps, and animation curves across 6+ iterations
  3. Restraint is design — removing gradient buttons, heavy shadows, and decorative elements made the site feel more premium
  4. CSS can do a lot — film grain, mask-image fades, backdrop blur, gradient text — no JS needed for most visual effects
  5. Mobile is a different design — every section needed explicit mobile treatment

The entire redesign happened in one session with Claude Code handling the implementation. The iterative loop — screenshot, feedback, adjust, screenshot — made it possible to converge on the right feel quickly.


The site is live at ai-journey-ten.vercel.app. The pre-redesign version is preserved as git tag v1.0-pre-nitro-redesign if you want to compare.