Architectural Review

Sandbox: Design Experiments

A Next.js-based playground for rapid visual prototyping. Self-contained experiments, data-driven gallery, no test suite, shipping design patterns.

1. The Shape

Framework & Deployment: Next.js 16.1.6 on Vercel. Server components + client components, SSR-rendered, live at joshcoolman.com. View Transitions API for page curtain effects (globals.css, lines 39–143). Dynamic routes under app/design-experiments/(experiments)/ — parenthesized folders for route layout organization.

Stack:

Build: npm run dev (port 3000), npm run build, npm run start. Next.js handles everything; no custom build step. Trailing slash disabled (next.config.js line 4). View Transitions experimental flag enabled (line 6).

Deployment: Vercel auto-builds on git push. Env vars for gated experiments (Monono, Chatroom). Rate limiting via Upstash Redis (optional local, required prod). Chatroom experiment also deploys a Cloudflare Worker with a Durable Object and SQLite state.

Discovery & Registration

Hand-curated array in lib/experiments/data.ts (228 entries). Each experiment is a TypeScript object with slug, date, title, subtitle, description, screenshot path, tags, and optional theme/colors.

FieldPurpose
slugURL route identifier and data key
dateReverse-chronological ordering
screenshotGallery thumbnail (next/image)
tagsFilter / discovery hint
theme / bgTop / bgBottomLight/dark mode + gradient overrides for gallery card

No manifest generator or build-time discovery. Gallery is always manually updated. This is deliberate: experiments live in app/design-experiments/(experiments)/ and are registered by hand in the data array. New experiment = add folder + add entry to data.ts.

Gallery Rendering

app/design-experiments/page.tsx (server) imports data and builds a runnable map, filtering gated experiments by env vars. GalleryClient.tsx (client) renders:

app/design-experiments/GalleryClient.tsx line 42–56: observer watches .experiment elements, adds visible class on 10% threshold, staggered by data-delay attribute.

Metadata & SEO

lib/experiments/metadata.ts: experimentMetadata(slug) returns Next.js Metadata object with title, description, OG image. Called by each experiment's page.tsx (e.g., app/design-experiments/(experiments)/leaderboard/page.tsx line 5). Root layout has fallbacks; per-page metadata overrides.

Environment Gating

lib/experiments/runnable.ts: hardcoded REQUIREMENTS map (lines 12–18) lists which experiments need which env vars. Monono needs VERCEL_AI_GATEWAY_KEY. Chatroom needs GATEWAY_KEY + WSS_URL + TICKET_SECRET. Galleries show placeholders for gated experiments with helpful error text.

Well-designed gating: Users see experiments are "there" and what's missing, not silent failures. Environment setup is honest and visible.

3. Per-Experiment Patterns

Typical per-experiment folder structure:

app/design-experiments/(experiments)/<name>/
├── page.tsx              # Server shell, metadata export
├── page.module.css       # Styles (or styles.css for global)
├── components/
│   ├── <Name>Content.tsx # Main client component
│   ├── Subcomponent.tsx  # Extracted components
│   └── *.module.css
├── hooks/
│   └── useCustom.ts
├── data/
│   ├── config.ts
│   └── items.ts
└── README.md             # Optional architect notes (Chatroom example)

Representative Experiments

CrossFit Bento (Feb 20) app/design-experiments/(experiments)/crossfit-bento/

CrossFit Design Challenge (Feb 9) app/design-experiments/(experiments)/crossfit-challenge/

Kobold Blaster (May 13) app/design-experiments/(experiments)/kobold-blaster/

Ripple Cycle (May 13) app/design-experiments/(experiments)/ripple-cycle/

Shared Patterns Across Experiments

4. Frontend Patterns

Component Organization

Per-experiment components live in (experiments)/<name>/components/. Shared components (home page, nav, footer) live in app/components/:

ComponentPurpose
CurtainLink.tsxView Transitions wrapper for page curtain effect
HomeExperimentCard.tsxHome page experiment card with screenshot
Sidebar.tsxSite navigation sidebar
SiteFooter.tsxFooter with social/links
NetworkCanvas.tsxAnimated background network on home

CurtainLink (lines 1–51) is elegant: wraps Next.js Link, intercepts clicks, calls document.startViewTransition() with custom data attributes, cleans up after. Enables the wipe-up/down curtain CSS animations (globals.css).

Styling Conventions

CSS Modules dominant: 78 .module.css files across experiments. Each imports fonts locally (via @import url, lines like crossfit-bento/page.module.css line 1) or relies on root layout fonts.

Design tokens: Per-experiment color palettes are inline (no global theme). Root globals.css (lines 1–11) defines --site-bg (#0d0d0d), --site-text, --light-* variants for light mode. Experiments override with their own palettes (crossfit-bento uses orange #e87000, brown #a06020, olive, etc.). Cards often have custom CSS vars for accent color.

Typography: Root layout injects 4 fonts via next/font/google (layout.tsx lines 6–32):

Each experiment may load additional fonts (DM Sans, Geist Pixel Square, etc.). No shared design system; each experiment is visually autonomous.

Animation Approach

Motion (Framer Motion successor): 17 experiments use it. Primarily for layout animations (reordering, stagger, springs). Leaderboard (line 7) uses Motion for row reordering when scores change. AnimatedCard in crossfit-bento wraps children with staggered delays.

GSAP: 15 experiments. ScrollTrigger for scroll-driven animations (card-stack, blend). Timelines for complex sequences. Ripple-cycle uses GSAP for the animation loop, not DOM elements.

Vanilla CSS: Most. @keyframes for entrance stagger (GalleryClient.tsx data-delay + CSS animation). View Transitions API for page transitions (globals.css).

No animation lock-in: Experiments freely mix GSAP, Motion, and vanilla CSS. Decisions are per-experiment, not architectural mandates. Reduces friction for rapid prototyping.

Image Handling

next/image for gallery thumbnails (GalleryClient.tsx line 126–132). Raw <img> or Canvas for experiment content. Public folder structure:

No image optimization pipeline. Assets are committed to repo. Screenshot URLs hardcoded in data.ts.

5. Cross-Cutting Concerns

Tooling & Linting

No ESLint, no Prettier. TypeScript strict mode (tsconfig.json, line 10). Vite in devDeps but not used in build (Next.js handles it). No test suite — intentionally omitted. This is a sketchbook, not production code.

Environment Management

.env.local.example (6 vars documented):

Env validation is implicit: experiments check process.env[key] and degrade gracefully (show placeholder or fallback behavior).

Rate Limiting & Cost Control

Monono + Chatroom experiments have per-session budget caps (Upstash Redis). Monono has a character limit (Haiku, cheap) and a per-session conversation budget. Chatroom has per-IP session counter + global $ cap. See runnable.ts and api/monono/route.ts for implementation.

Deployment & Monitoring

Vercel Analytics (layout.tsx line 84). Chatroom experiment also deploys a Cloudflare Worker (separate repo, not in this tree). Git push → Vercel auto-deploy. No staging environment; main branch is live.

Security

Chatroom uses signed HMAC tickets for WS upgrade (runnable.ts TICKET_SECRET). API routes check cost caps. .env.local is git-ignored. No public API keys exposed (Vercel AI Gateway key is secret).

Single-env deployment: No staging; main branch is production. Experiments ship directly. For personal sandbox, acceptable; wouldn't scale to team.

6. Patterns Worth Stealing

Data-Driven Gallery with Env Gating

Single TypeScript array (experiments/data.ts) controls what appears where. Runnable check determines if full card or placeholder renders. Gated experiments are visible but explain what's missing. Zero "silent failures" or hidden routes.

Reusable in: genzen (gating which models are runnable), any tool that ships experimental features.

View Transitions API for Page Curtain

CurtainLink intercepts clicks, starts document.startViewTransition(), updates DOM. CSS animations (clip-path wipe) run during transition. Graceful fallback if browser doesn't support. Per-link opt-in, no framework overhead.

app/components/CurtainLink.tsx, app/globals.css lines 39–143

SessionStorage Scroll Restoration

When you click an experiment, save slug to sessionStorage. On back, scroll to that experiment's id in DOM. IntersectionObserver re-triggers animations only on first visit (hasVisited flag). Feels fast and predictable.

app/design-experiments/GalleryClient.tsx lines 32–72

Decoupled Art + Code Pipelines (Kobold Blaster)

Claude wrote code (game loop, physics, rendering). GPT-image-1 generated sprites (text prompts → PNG sheets). Neither touched the other's output. Pixel scanner auto-derived crop bounds. Shows AI systems can work in parallel without tight coupling.

app/design-experiments/(experiments)/kobold-blaster/components/KoboldBlaster.tsx lines 18–49

CSS Module Scope Without Design System

Each experiment is visually autonomous. No shared component library, no theme file. Reduces friction for rapid iteration. Experiments can export portable components (card-stack/index.ts exports CardStack) when mature enough.

Typical pattern: app/design-experiments/(experiments)/retro-tech/page.module.css

Motion Layout Animations for Reordering

Leaderboard uses Motion's layout prop to animate row position changes. No manual spring math. Pass delay and overshoot, Motion handles the choreography. Cheap, reliable, feels responsive.

app/design-experiments/(experiments)/leaderboard/components/LeaderboardRow.tsx

Metadata Export Per Page

Each experiment exports Next.js Metadata with title + description. experimentMetadata(slug) helper looks up data.ts entry. OG image URLs point to public/screenshots/. Consistent, maintainable, crawlable.

lib/experiments/metadata.ts

WebGL2 Fragment Shaders for Visual Effects

Ripple Cycle demonstrates that custom shaders are tractable for one-off effects. Aspect-aware math, smooth falloffs, clean start/end. GLSL is verbose but expressive for creative work.

app/design-experiments/(experiments)/ripple-cycle/page.tsx lines 21–78

7. Concerns & Red Flags

Manual gallery registration: Adding an experiment requires edits to three places: folder creation, data.ts entry, and (for SEO) README.md/sitemap updates. Risk of desynchronization. Could be automated via folder scan + metadata frontmatter, but manual is fine for slow-moving personal project.
No build-time asset optimization: Screenshots are committed to git. Sprites (kobold-blaster, monono) are committed. Repo size will grow. For a personal sandbox, acceptable; not viable at scale.
Tight coupling to joshcoolman.com domain: Metadata hardcoded (layout.tsx line 35). Changing domain requires edits. Fine for personal site; wouldn't fork cleanly.
No TypeScript at component boundaries: Many experiments accept loose props or don't export component types. Moving a component to portable status requires adding proper types. Workable but adds friction to extraction.
Env vars are implicit: runnable.ts has a hardcoded REQUIREMENTS map. Adding a new gated experiment requires manual entry. Could be auto-detected via folder scan or file marker (e.g., // @requires VERCEL_AI_GATEWAY_KEY). Low risk but worth documenting.
No staging environment: Experiments ship to production immediately. This is intentional (sandbox philosophy), but means a broken experiment is visible to visitors. Acceptable trade-off.

8. What This Really Is

Not just "experiments folder." Sandbox is a design playground and code exhibition for one person learning in public. The real product is:

The workflow from CLAUDE.md:

  1. /sketch — rapid prototype
  2. /ship-experiment — screenshot, gallery entry, commit, push

/ship-experiment is a Claude Code skill that:

The skill lives outside this repo, but it knows where to write (data.ts, public/screenshots/, README.md paths). Evidence: git log shows "ship" commits (e.g., a9e62f5 "Ripple Cycle: WebGL ripple-transition experiment" on May 13).

9. Key File Map

FileLinesWhy Read It
package.json1–42All dependencies. GSAP, Motion, Anthropic SDK, Vercel Analytics, sharp-cli.
next.config.js1–10View Transitions enabled, trailing slash disabled, no rewrites or redirects.
app/layout.tsx1–89Root fonts (Karla, Bitter, Lora, Space Mono), metadata defaults, Analytics injection.
app/globals.css1–161Site-wide tokens (--site-bg, --site-text), View Transitions animations (curtain wipe + fade), reduced-motion fallback.
lib/experiments/data.ts1–228Gallery registry. 25 experiments with metadata. Manually maintained.
lib/experiments/runnable.ts1–48Env var gating logic. REQUIREMENTS map lists which experiments need which keys.
lib/experiments/metadata.ts1–17Per-experiment SEO metadata helper. Called by each experiment's page.tsx.
app/design-experiments/page.tsx1–20Gallery server shell. Builds runnable map and passes to client.
app/design-experiments/GalleryClient.tsx1–205Gallery rendering. IntersectionObserver stagger, sessionStorage scroll restoration, placeholder rows for gated experiments.
app/components/CurtainLink.tsx1–51View Transitions wrapper. Intercepts clicks, triggers startViewTransition, cleans up data attributes.
app/design-experiments/(experiments)/crossfit-bento/page.tsx1–10Typical experiment server shell. Metadata + Content component render.
app/design-experiments/(experiments)/crossfit-bento/components/CrossfitBentoContent.tsx1–100+Complex example: Motion + CSS Modules + data viz (heatmap, charts, count-up).
app/design-experiments/(experiments)/ripple-cycle/page.tsx21–78WebGL2 shaders. Aspect-aware ripple, smooth falloff, UOrigin uniform for click tracking.
app/design-experiments/(experiments)/kobold-blaster/components/KoboldBlaster.tsx18–49Sprite crop system. Demonstrates decoupled art + code pipelines.
app/design-experiments/(experiments)/leaderboard/components/LeaderboardRow.tsxMotion layout animation example. Row reordering with spring.
app/design-experiments/(experiments)/chatroom/README.md1–80Architecture document for ambitious experiment. Phase milestones, Durable Object design, SQLite state, WS hibernation.
.env.local.example1–15Required + optional env vars. Vercel AI Gateway, Upstash Redis, Chatroom WSS, HMAC secret.
CLAUDE.md1–55Repo conventions. Workflow (sketch → ship-experiment). Structure. Component extraction thresholds.
app/types/experiments.ts1–12Experiment TypeScript interface. All gallery metadata flows from this type.
Last regenerated on 2026-05-21 via the /explore-repo Claude skill.