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:
- Next.js 16.1.6 with React 19, TypeScript 5.9
- CSS Modules + raw CSS (no Tailwind lock-in)
- Animation: GSAP (ScrollTrigger, timeline), Motion (layout animations), vanilla JS/Canvas/WebGL
- Styling system: 4 Google Fonts (Karla, Bitter, Lora, Space Mono) + custom color tokens (globals.css)
- AI integration: Anthropic SDK (monono, chatroom experiments)
- Data sources: Markdown files, TypeScript data objects (static), Cloudflare Durable Objects (chatroom)
- Analytics: Vercel Analytics
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.
2. Gallery & Experiments Architecture
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.
| Field | Purpose |
|---|---|
| slug | URL route identifier and data key |
| date | Reverse-chronological ordering |
| screenshot | Gallery thumbnail (next/image) |
| tags | Filter / discovery hint |
| theme / bgTop / bgBottom | Light/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:
- ExperimentRow for runnable experiments: screenshot thumbnail (next/image), title, date, full description, tag pills
- PlaceholderRow for gated experiments: "requires setup" stub, lists missing env vars
- IntersectionObserver for lazy stagger animations (visible class toggled on scroll)
- SessionStorage scroll restoration: returns user to same card after clicking back
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.
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/
- What it demos: Dark bento grid dashboard (3-col, gap-14px), nine data viz widgets, animated count-up numbers, heatmap with randomized data, GitHub-style activity grid with flame icons.
- Techniques: CSS Module grid (page.module.css lines 18–24), Motion layout animations (useCountUp hook line 49), modulo coloring for heatmap (line 56), tabbed state (calories consumed/burned toggle).
- Fonts: DM Sans body + Geist Pixel Square for technical labels (monospace system metric display).
- Components: ProgressRing (SVG circle with rotation transform), StackedBarChart, DonutChart, Heatmap (grid of colored divs with random 0–4 scale), MetricTile, AnimatedCard (Motion wrapper with stagger delay).
- Reusable: ProgressRing, heatmap randomizer, chart components — extracted and could be portable to other dashboards.
CrossFit Design Challenge (Feb 9) — app/design-experiments/(experiments)/crossfit-challenge/
- What it demos: Four agentic designers each produced a dark-mode gym homepage. Day 2 iteration: added constraints (dark, animation, data viz). Gallery of cards that scale-transform to preview, click to fullscreen each design.
- Techniques: Global styles.css (no modules) with CSS animations (glitch, scroll reveals, chart animations). Four design components (Brutal, Minimal, Editorial, TechData) each with unique CSS feel. Persona data in TypeScript (designers.ts).
- Interesting: Each design is a complete standalone page component. Scale(0.35) on card preview with CSS origin. Accent color per designer (CSS var --accent).
- Code pipeline story: Showcase how different personas produce different outputs from same prompt. Shows "agentic designer" as a viable workflow.
Kobold Blaster (May 13) — app/design-experiments/(experiments)/kobold-blaster/
- What it demos: Canvas 2D horde shooter. Carl + Princess Donut vs. kobold waves. Chain explosions, combo scoring, CRT overlay, pixel death sequences.
- Techniques: Handwritten game loop (KoboldBlaster.tsx line 1–), sprite crops (hardcoded tight bounding boxes per frame), canvas rendering, physics for bombs.
- Sprite system: CARL_CROPS, KOBOLD_CROPS, DONUT_CROPS — arrays of [sx, sy, sw, sh] cropping rectangles (lines 18–49). Animation frames defined as row/cols/fps. GPT-image-1 generated the sprite sheets; Claude wrote the crop derivation.
- Clever: Decoupled art + code pipelines. GPT-image-1 generated sprites from prompts, returned chroma-keyed PNG. Claude built a pixel scanner to auto-crop tight bounds.
Ripple Cycle (May 13) — app/design-experiments/(experiments)/ripple-cycle/
- What it demos: Click anywhere, radial ripple wave distorts 6 photos, cross-fade to next. WebGL2 + GSAP animation.
- Techniques: Custom vertex + fragment shaders (VERT_SRC, FRAG_SRC lines 21–78). Aspect-aware ripple (corrects canvas AR so ripple stays circular). Sin-wave displacement with smooth falloff. Sampler with object-fit:contain letterbox logic.
- Interesting: uOrigin uniform tracks click coordinates. Wave frequency and ripple strength are tunable. Falloff guarantees clean start/end (no shimmer artifacts). Shows WebGL2 is still viable and clean for visual effects.
Shared Patterns Across Experiments
- Page.tsx shell: Always imports experimentMetadata(slug), renders
<Content />component. Server component. - Content component: Marked 'use client', imports CSS Modules, renders the UI tree.
- CSS Modules for layout: Most use page.module.css. Some use component-scoped .module.css files.
- Global styles.css: Only in a few (crossfit-challenge, ripple-cycle, etc.). Used when global animations or resets are needed.
- Animation libraries: GSAP (15 experiments), Motion (17 experiments), vanilla CSS (most).
- Image handling: next/image for thumbnails, public/ folder for experiment assets (public/design-experiments/*, public/ascii-reveal/*, etc.).
4. Frontend Patterns
Component Organization
Per-experiment components live in (experiments)/<name>/components/. Shared components (home page, nav, footer) live in app/components/:
| Component | Purpose |
|---|---|
| CurtainLink.tsx | View Transitions wrapper for page curtain effect |
| HomeExperimentCard.tsx | Home page experiment card with screenshot |
| Sidebar.tsx | Site navigation sidebar |
| SiteFooter.tsx | Footer with social/links |
| NetworkCanvas.tsx | Animated 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):
- Karla (300–600 weight) — default body font
- Bitter (700–800 weight) — headings, serif
- Lora (400–500, italic) — editorial
- Space Mono (400–700) — monospace
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).
Image Handling
next/image for gallery thumbnails (GalleryClient.tsx line 126–132). Raw <img> or Canvas for experiment content. Public folder structure:
- public/screenshots/ — gallery thumbnails (280x210)
- public/design-experiments/* — per-experiment assets (kobold sprites, ascii photos, etc.)
- public/ascii-reveal/ — portrait photos
- public/staff-photos/ — contact-sheet experiment images
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):
- VERCEL_AI_GATEWAY_KEY — Anthropic gateway (required for Monono)
- UPSTASH_REDIS_REST_* — Rate limiting (optional local, required prod)
- NEXT_PUBLIC_CHATROOM_WSS_URL — Cloudflare Worker WSS endpoint
- TICKET_SECRET — HMAC for signed WS upgrade tickets
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).
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
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:
- Rapid iteration loop: Idea → folder + page.tsx → gallery entry → screenshot → commit → live. Minimal ceremony.
- Visual communication: Show what you build, not just write about it. Gallery + README.md showcase each work. GitHub links expose the code.
- Pattern catalogue: 25 experiments covering CSS, WebGL, Canvas, React animations, AI chat, data viz, layout grids, UI components, agentic design. Each is a case study.
- Portability testing: Some experiments (Leaderboard, Retro Tech, Sticky Notes, Contact Sheet) are extracted to app/design-experiments/(experiments)/<name>/index.ts as importable components. Tests whether patterns are reusable.
- Agentic design showcase: Chatroom + CrossFit Challenge + Kobold Blaster show AI agents as active collaborators (not just tools). Multiple personas, parallel work, decoupled pipelines.
The workflow from CLAUDE.md:
- /sketch — rapid prototype
- /ship-experiment — screenshot, gallery entry, commit, push
/ship-experiment is a Claude Code skill that:
- Screenshots the experiment at a standard size
- Adds an entry to lib/experiments/data.ts
- Creates/updates README.md in the experiment folder
- Commits and pushes
- Triggers Vercel deploy
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
| File | Lines | Why Read It |
|---|---|---|
| package.json | 1–42 | All dependencies. GSAP, Motion, Anthropic SDK, Vercel Analytics, sharp-cli. |
| next.config.js | 1–10 | View Transitions enabled, trailing slash disabled, no rewrites or redirects. |
| app/layout.tsx | 1–89 | Root fonts (Karla, Bitter, Lora, Space Mono), metadata defaults, Analytics injection. |
| app/globals.css | 1–161 | Site-wide tokens (--site-bg, --site-text), View Transitions animations (curtain wipe + fade), reduced-motion fallback. |
| lib/experiments/data.ts | 1–228 | Gallery registry. 25 experiments with metadata. Manually maintained. |
| lib/experiments/runnable.ts | 1–48 | Env var gating logic. REQUIREMENTS map lists which experiments need which keys. |
| lib/experiments/metadata.ts | 1–17 | Per-experiment SEO metadata helper. Called by each experiment's page.tsx. |
| app/design-experiments/page.tsx | 1–20 | Gallery server shell. Builds runnable map and passes to client. |
| app/design-experiments/GalleryClient.tsx | 1–205 | Gallery rendering. IntersectionObserver stagger, sessionStorage scroll restoration, placeholder rows for gated experiments. |
| app/components/CurtainLink.tsx | 1–51 | View Transitions wrapper. Intercepts clicks, triggers startViewTransition, cleans up data attributes. |
| app/design-experiments/(experiments)/crossfit-bento/page.tsx | 1–10 | Typical experiment server shell. Metadata + Content component render. |
| app/design-experiments/(experiments)/crossfit-bento/components/CrossfitBentoContent.tsx | 1–100+ | Complex example: Motion + CSS Modules + data viz (heatmap, charts, count-up). |
| app/design-experiments/(experiments)/ripple-cycle/page.tsx | 21–78 | WebGL2 shaders. Aspect-aware ripple, smooth falloff, UOrigin uniform for click tracking. |
| app/design-experiments/(experiments)/kobold-blaster/components/KoboldBlaster.tsx | 18–49 | Sprite crop system. Demonstrates decoupled art + code pipelines. |
| app/design-experiments/(experiments)/leaderboard/components/LeaderboardRow.tsx | – | Motion layout animation example. Row reordering with spring. |
| app/design-experiments/(experiments)/chatroom/README.md | 1–80 | Architecture document for ambitious experiment. Phase milestones, Durable Object design, SQLite state, WS hibernation. |
| .env.local.example | 1–15 | Required + optional env vars. Vercel AI Gateway, Upstash Redis, Chatroom WSS, HMAC secret. |
| CLAUDE.md | 1–55 | Repo conventions. Workflow (sketch → ship-experiment). Structure. Component extraction thresholds. |
| app/types/experiments.ts | 1–12 | Experiment TypeScript interface. All gallery metadata flows from this type. |