>Section Title</
h2
>
</FadeIn>
<StaggerChildren>
<StaggerItem>Card 1</StaggerItem>
<StaggerItem>Card 2</StaggerItem>
</StaggerChildren>
FadeIn uses IntersectionObserver to trigger when elements scroll into view, with a Safari fallback that manually checks getBoundingClientRect after a short delay. Safari mobile occasionally fails to fire the initial observer callback during SPA navigations — a bug I only caught through device testing.
StaggerChildren and StaggerItem use Framer Motion's variant propagation to cascade animations through a list with a 100ms delay between items. The result is a subtle wave effect that makes lists feel intentional rather than static.
Design Decisions
Dark mode first. I designed in dark mode and adapted for light. Most developer portfolios look better dark, and it forced me to think about contrast and hierarchy before reaching for colour.
Minimal colour. The palette is almost entirely neutral — greys, whites, and a single blue accent used sparingly. Colour is reserved for interactive states (hover, focus) and status indicators. This keeps the focus on content.
Consistent spacing. Every page uses the same container pattern — max-w-7xl with responsive padding. Section separators use Separator components with consistent vertical margins. It's boring, but boring spacing is invisible, and that's the point.
One pattern I'm pleased with is the shared PageHeader component. Every interior page (About, Blog, Projects, Contact) uses it:
<PageHeader
title="About Me"
subtitle="A bit about who I am and what I do."
>
<ProfileImage />
</PageHeader>
It renders a full-bleed background section with consistent padding and typography. The optional children prop lets pages like About add a profile image alongside the title. This kind of small abstraction keeps the codebase consistent without adding complexity.
Content Management
Blog posts live as .mdx files in a content/blog/ directory. At build time, gray-matter parses the frontmatter, reading-time calculates the reading time, and the posts are sorted by date. No CMS, no database, no build-time API calls. The content is co-located with the code and version-controlled in Git.
For syntax highlighting, I use Shiki with GitHub's light and dark themes. The highlighting runs at build time via rehype-shiki, so there's no client-side JavaScript for code blocks. The HTML arrives pre-highlighted and switches themes via CSS custom properties.
What I'd Do Differently
Start with the blog layout. I built the homepage and projects page first, then retrofitted the blog. Starting with the content-heavy pages would have established typography and spacing patterns earlier.
Design the dark and light themes simultaneously. Adapting dark-first designs to light mode occasionally produced washed-out results. Designing both in parallel would have caught those issues earlier.
The site is intentionally simple. No CMS, no analytics dashboard, no complex build pipeline. It's a Next.js app with Tailwind, deployed to Vercel, and it builds in under 10 seconds. That simplicity is a feature.