Loading
Colophon
Every decision has a reason. Most of them are documented here. This is the page for the curious — the ones who hovered long enough to find it.
01 · Stack
Next.js 16 (App Router)
Framework · SPA routing · server + client components
TypeScript
Type safety · the boring kind that saves hours later
Tailwind CSS v4
Styling · @theme CSS tokens · golden ratio scale
Framer Motion
Every animation on this site
Supabase
Database · real-time · row level security · 7 tables
Vercel Blob
Post image storage
Admin CMS
Custom-built · no third-party headless CMS · lives at /admin
DM Sans
Display typeface for 'Ojas Mutreja' in hero · geometric sans · next/font/google
MDX
Fragments content system
gray-matter
Frontmatter parsing for fragments
Vercel
Deployment · edge network
GitHub
Version control · 5 commits from zero to this
No backend. No database. No CMS. Everything is either static content, MDX files, or client-side state. The simplest architecture that could possibly work is usually the right one.
02 · Animations
Every animation on this site has a name. This is not precious — it is practical. Named things get designed. Unnamed things get approximated.
The page transition. A dark (#1C1C1A) curtain that wipes up to cover the screen on exit, then retracts from the top on enter. Built with Framer Motion AnimatePresence and a custom TransitionLink component that intercepts all internal navigation. The key detail: the animation completes before the URL changes. Most page transitions fire simultaneously with navigation — they feel cheap because the new page flashes through before the curtain closes. This one does not. Easing: cubic-bezier(0.76, 0, 0.24, 1). Duration: 500ms each way. Total transition time: about 1.2 seconds. Worth it.
The grain overlay. A fixed-position full-screen div with an SVG feTurbulence filter applied at 4% opacity with mix-blend-mode: overlay. The filter is defined once in the root layout as invisible infrastructure. The overlay references it through CSS. Why 4%? Below 3% it disappears. Above 5% it becomes a filter rather than a texture. At 4% it becomes a physical property of the surface. Try it yourself on the /playground page.
The custom cursor. Two elements: a small ink dot (8px) that follows the mouse exactly, and a larger ring (32px) that follows with a spring lag through Framer Motion useSpring. On hover targets the dot scales to zero and the ring fills. On dark surfaces the colors invert through CSS blend logic. The system cursor is hidden globally. The Ghost takes over entirely.
The Badge · inline hero element · SVG textPath circular text · CSS rotate animation · image with radial gradient fallback · spin duration changes on hover via CSS custom property
A full-section HTML5 canvas starfield with a gradient mask layered above it. Eighty stars are generated once with a mulberry32 seeded PRNG (seed: 42), then only the first forty render on mobile with reduced opacity and no constellation lines. Five simple constellations are hard-positioned into the right side of the canvas, each fading independently on distinct 15–30 second cycles through slow opacity interpolation. Tablet pushes those constellations upward; mobile compresses them toward the upper center-right. The lower stars never hard-cut. They fade into the background through the mask.
Horizontal scrolling project cards on /work. CSS scroll-snap-type: x mandatory with scroll-snap-align: start on each card. The scrollbar is hidden through scrollbar-width: none in Firefox and ::-webkit-scrollbar in Chromium and Safari. No JavaScript. No library. The browser handles all momentum, snapping, and touch behavior natively. The hint text says drag or scroll to explore. It means what it says.
Scroll-triggered section reveals across the site. Framer Motion whileInView with initial opacity 0 and y: 20px, animating to opacity 1 and y: 0. Triggered once rather than every time the element re-enters the viewport. The threshold is 0.1. Small stagger delays between sibling elements create a cascade without tipping into slideshow territory.
Project card hover state on /work. translateY: -6px with box-shadow: 0 30px 80px rgba(0,0,0,0.1). Transition: 400ms cubic-bezier(0.16, 1, 0.3, 1), which feels physical rather than mechanical. The arrow shifts 4px right at the same time. It is a small motion. Small motions matter more because they happen often.
03 · Design
Type
Three typefaces. Fraunces for display, titles, and headings — a variable serif with an optical size axis that makes large type expressive and small type quietly readable. Plus Jakarta Sans for body and captions — humanist, clear, never shouting. DM Sans appears once, on the homepage nameplate, where a cleaner geometric sans gives the hero a sharper contrast against Fraunces. Every size is derived from a single root (128px) divided by phi recursively, producing six sizes: 128 · 79 · 49 · 30 · 18.5 · 11.5px. The same ratio governs spacing. Nothing is arbitrary. This is why the site feels proportionally coherent without anyone being able to explain why.
Ojas
display · 128px
Mutreja
title · 79px
Builder
heading · 49px
Designer
sub · 30px
Software and design, built for humans.
body · 18.5px
The Texture · SVG feTurbulence · 4% opacity
caption · 11.5px
Color
#EFEFED
#E8E8E6
#C4C4C2
#9A9A98
#0A0A0A
Five colors. One is black. One is close to black. The rest are gray. The accent color is the absence of gray. This is either very restrained or very lazy — the site's job is to make the content interesting, not the palette.
04 · Philosophy
This site is a Single Page Application. The browser never actually navigates. Next.js intercepts routing, renders pages client-side, and manages history through the History API. When you click a link, no server is contacted. The curtain is fake. The page change is a React component swap. It just feels better.
Why does it feel better? Because real navigation has latency. It has flash. It has the visual discontinuity of a full document reload. An SPA removes all three. Transitions become choreography rather than loading states.
The grain, the custom cursor, the rotating badge text, the star haze at the top of a section — none of these communicate information. They communicate care. They tell the visitor that someone made a hundred small decisions about how this would feel, not just how it would work.
Details compound. Nobody notices a single well-crafted pixel. Everyone notices when a hundred of them are wrong. The work is in the accumulation.
The content management system is custom-built and lives at /admin — a password-gated interface for writing fragments, posts, managing projects, and editing every editable part of this site. No third-party CMS. No Notion integration. No markdown files committed to git. Everything lives in a Supabase database and updates instantly without a redeploy. The /now page changes in under a second. That matters more than it sounds.
Yes, the curtain is fake. It just feels better.
05 · Credits
Fraunces by Undercase Type and Octavia Saul. Plus Jakarta Sans by Tokotype. Next.js by Vercel. Framer Motion by Matt Perry. The /now page format by Derek Sivers. The idea that details matter: everyone who ever shipped something worth remembering.
Built by Ojas Mutreja · ojasmutreja.com · view source on GitHub →