A Next.js 16+ personal portfolio template built around a signature WebGL flow shader, a magnetic morphing portrait, Lenis smooth scroll, and a calm black-and-white design system. Designed for individual designers and engineers who want a brand-ready scaffold with a distinctive look on day one.
- ✅ Next.js 16+ with App Router
- ✅ TypeScript (strict mode)
- ✅ Tailwind CSS v4 with token-driven theming
- ✅ Dark Mode via next-themes (class-based) with view-transition reveal
- ✅ Motion via motion/react with reduced-motion support
- ✅ WebGL Flow Shader — aspect-correct circular fade baked into the fragment, theme-aware bg sync
- ✅ Lenis Smooth Scroll with anchor-link integration
- ✅ Portrait Morph — hover-driven webp swap with magnetic cursor follow
- ✅ Polaroid Strip, Skills, Stack, Experience, Education — co-located content sections for an
/aboutroute - ✅ Projects Grid — dribbble-style mockup cards with hover lift and image zoom
- ✅ Contact Card — single-click copy email with hover content swap, embedded shader
- ✅ Animated Pill Nav — spring-animated active indicator, hydration-safe theme toggle
- ✅ Site Frame — fixed top/left/right rails with rounded inner corners
- ✅ SEO Ready — metadata, Open Graph, Twitter cards, sitemap, robots
- ✅ Accessibility — skip links, focus rings, ARIA labels,
prefers-reduced-motionguards - ✅ Edge Compatible — no Node-only APIs
- Nav — Fixed pill nav with spring-animated active indicator and hydration-safe theme toggle
- Hero — WebGL flow shader backdrop, two-line headline, morphing portrait, magnetic CTAs
- Projects — Grid of dribbble-style project cards with hover lift, image zoom, and external links
- About — Polaroid strip, skills grid, interactive Matter.js stack chips, expandable experience timeline, education list
- Contact Card — Embedded shader, copy-to-clipboard email, secondary social CTAs
- Page Backdrop — Site-wide flow shader, mobile-attenuated, baked radial fade to background
- Skip-to-Content — Keyboard-first accessibility entry point
npm installnpm run devOpen http://localhost:3000 in your browser.
| Command | Description |
|---|---|
npm run dev |
Start development server |
npm run build |
Build for production |
npm run start |
Start production server |
npm run lint |
Run ESLint |
npm run lint:fix |
Fix ESLint errors |
npm run format |
Format code with Prettier |
npm run format:check |
Check code formatting |
npm run typecheck |
Run TypeScript type checking |
├── app/
│ ├── about/ # About route
│ ├── projects/ # Projects route
│ ├── globals.css # Design tokens, frame, project-card styles
│ ├── layout.tsx # Root layout with providers, nav, backdrop
│ ├── page.tsx # Home page
│ ├── robots.ts # Dynamic robots.txt
│ ├── sitemap.ts # Dynamic sitemap
│ ├── icon.svg # Favicon
│ └── apple-icon.svg # Apple touch icon
├── components/
│ ├── about/
│ │ ├── education.tsx # Education list with bordered logo squares
│ │ ├── experience.tsx # Expandable timeline with fade-mask collapse
│ │ ├── polaroid-strip.tsx # Tilted polaroid photos with dotted backs
│ │ ├── skills.tsx # Skills grid
│ │ └── stack.tsx # Matter.js physics-driven tech chips
│ ├── contact/
│ │ ├── contact-button.tsx # Click-to-copy email button
│ │ ├── contact-card.tsx # Shader-backed contact card
│ │ └── contact-card-ctas.tsx # Social CTAs
│ ├── hero/
│ │ ├── hero.tsx # Hero layout and copy
│ │ ├── hero-ctas.tsx # Magnetic primary/secondary CTAs
│ │ └── portrait-morph.tsx # Hover-swap portrait with magnetic follow
│ ├── layout/
│ │ ├── nav.tsx # Pill nav with theme toggle
│ │ ├── page-backdrop.tsx # Site-wide shader backdrop
│ │ ├── providers.tsx # Theme + smooth-scroll providers
│ │ ├── skip-to-content.tsx # Skip link for a11y
│ │ └── smooth-scroll.tsx # Lenis smooth-scroll wrapper
│ ├── projects/
│ │ └── projects.tsx # Projects grid
│ ├── shaders/
│ │ └── shader-flow.tsx # WebGL flow shader (raw OGL)
│ └── ui/
│ ├── dotted-pattern.tsx # Shared dotted texture
│ └── motion-primitives.tsx # FadeIn, ScaleUnblur entrance helpers
├── lib/
│ ├── config.ts # Site config
│ ├── metadata.ts # SEO metadata utilities
│ └── motion.tsx # Motion components & hooks
└── public/
├── josh.webp # Default portrait
├── josh_wave.webp # Hover portrait
├── linkedin.svg # Social icon
├── x.svg # Social icon
└── site.webmanifest # PWA manifest
Edit lib/metadata.ts to update:
- Site name, description, and URL
- Social media handles
- Keywords and authors
The default siteConfig.url is https://example.com — replace it with your production URL before deploying so OpenGraph and the sitemap emit correct absolute URLs.
- Swap
public/josh.webpandpublic/josh_wave.webpwith your own default + hover portraits. Keep both files the same dimensions and aspect ratio for a clean morph. - Update headline, eyebrow, and subtitle copy in
components/hero/hero.tsx. - Update social handles and email in
components/contact/contact-card.tsxandcomponents/contact/contact-card-ctas.tsx.
All about-page content is co-located in its component file — there is no separate content directory.
components/about/polaroid-strip.tsx— image paths and captionscomponents/about/skills.tsx— skill listcomponents/about/stack.tsx— tech logos and physics chipscomponents/about/experience.tsx— roles, companies, dates, descriptionscomponents/about/education.tsx— schools, programs, dates
Edit the project array in components/projects/projects.tsx. Each entry includes a title, description, image (dribbble mockup or your own), and external link.
Replace the following files with your brand assets:
app/icon.svg— Favicon (32x32)app/apple-icon.svg— Apple touch icon (180x180)public/og-image.png— Open Graph image (1200x630)
The flow shader (components/shaders/shader-flow.tsx) is used by both the page backdrop and the contact card. Key knobs (all exposed as props with sensible defaults):
colorLowA,colorHighA— palette stops in linear RGBflowSpeed—[x, y]flow vectoriterations— domain-warp iteration count (up to 24)scale— domain scalebrightness— output multiplierfadeCx,fadeCy,fadeRx,fadeRy— aspect-correct circular fade center and radii. The fade is baked into the fragment shader and reads--backgroundfrom CSS so theme changes are picked up automatically via aMutationObserveron<html>.
The shader:
- Caps DPR at
min(devicePixelRatio, 1.25)forShaderFlow - Sizes to host container via
ResizeObserver - Pauses via
IntersectionObserverwhen offscreen and onvisibilitychange - Uses
highpprecision; renders a single opaque draw (no CSS mask layers)
// app/contact/page.tsx
import { createMetadata } from "@/lib/metadata";
import type { Metadata } from "next";
export const metadata: Metadata = createMetadata({
title: "Contact",
description: "Get in touch.",
path: "/contact",
});
export default function ContactPage() {
return <main id="main-content">...</main>;
}--background/--foreground— Page background and text--muted/--muted-foreground— Subtle surfaces and secondary text--border— Hairline rails and dividers--ring— Focus rings--frame— Site-frame color (matches--background)
The palette is strict black and white. No accent or semantic color hues are used.
- Sans: Geist Sans
- Mono: Geist Mono
- Serif: Fraunces (used selectively for display headlines)
- Headlines: maximum two lines, no italic, no em-dashes
- Site frame: fixed top + left + right rails with two top corner cutouts (desktop only; hidden under 850px)
- Card hovers: project cards use a single resting + single hover shadow tier with translate-y lift
- No
backdrop-blur(except the experience collapsed-fade) - Cursor:
pointeron all clickable nav and CTA items
The template includes:
- Skip-to-content link
- Visible focus rings on all interactive elements
- ARIA labels on toggles, social links, and the contact button
prefers-reduced-motionguards on the theme toggle view-transition- Shaders pause when offscreen and on tab hide
- Proper heading hierarchy (single
<h1>per page) - WCAG 2.1 AA contrast compliance in both themes
- WebGL context cleanup on unmount via
WEBGL_lose_context - Single mount-once shader effect; uniforms updated via refs
- Shaders pause when offscreen (
IntersectionObserver) and on tab hide (visibilitychange) - DPR capped to keep shading cost bounded on retina displays
- Page-backdrop fade is baked into the fragment shader (single opaque draw, no mask layers)
- Matter.js is dynamic-imported inside the stack section
- Lenis smooth scroll runs on a single rAF loop
- Edge-compatible runtime
- The
next.config.tsimages.remotePatternsallowsimages.unsplash.comandcdn.dribbble.comfor the polaroid and project mockups respectively. - The portfolio uses a single
@/path alias. - The project image cards use a disclaimer comment at the top of
components/projects/projects.tsxnoting that dribbble mockups are placeholders to be replaced with your own work.
This template is free to use in personal and commercial projects. You may not resell or redistribute the template itself.