Overview

A "note to self" sticky note stack. Short thoughts (1-2 paragraphs) rendered as post-it notes. Click the stack to open a full-size lightbox. Click near the card to advance forward, click left of the card to go back, click far away (above/below) or press Escape to close. Stack position persists across client-side navigation, resets on hard refresh. Works out of the box with no configuration -- just pass notes.

Content Model

Markdown files in any directory, one per thought. The consumer passes the path to getAllNotes(). Minimal frontmatter:

---
date: 2026-02-18
color: warm  # optional: warm (default) | cool | neutral
---
Note content here. No title -- stickies don't have titles.

gray-matter auto-parses YAML dates into Date objects. loadNotes.ts converts them back to ISO date strings (YYYY-MM-DD) to avoid serialization issues.

Key Components

  • app/design-experiments/sticky-notes/types.ts -- StickyNote interface (id, date, color, content)
  • app/design-experiments/sticky-notes/loadNotes.ts -- getAllNotes(notesDir), accepts path to notes directory
  • app/design-experiments/sticky-notes/components/StickyNoteStack.tsx -- client component, all interaction logic
  • app/design-experiments/sticky-notes/components/stickyNotes.module.css -- all styling including animations
  • app/design-experiments/sticky-notes/index.ts -- barrel export
  • app/design-experiments/sticky-notes/page.tsx -- experiment showcase page
  • app/design-experiments/sticky-notes/data/ -- demo notes for the showcase

Architecture

Component location: Design experiment at app/design-experiments/sticky-notes/. Blog imports via barrel export. The loader accepts a notesDir parameter so any consumer can point to their own content.

// Usage -- just two imports, one prop
import { getAllNotes, StickyNoteStack } from '@/app/design-experiments/sticky-notes'

const notes = getAllNotes(path.join(process.cwd(), 'app/(blog)/notes'))

<StickyNoteStack notes={notes} />

Blog notes: app/(blog)/notes/*.md -- content that belongs to the blog, not to the component.

Demo notes: app/design-experiments/sticky-notes/data/*.md -- sample notes for the experiment showcase, one per color variant.

Stack (collapsed): 180x140px cards, offset 3px right + 3px down per card, slight rotation. Top card shows teaser text. Hover lifts top card. Click opens the lightbox -- no other controls on the collapsed stack.

Lightbox: Full-viewport slide transitions between notes. Click zones determine intent based on proximity to the card:

  • On card or within 100px left/right: advance (right side forward, left side back)
  • Above or below the card: close
  • Escape key: close

Current card slides out, next slides in. Forward goes left-to-right, back goes right-to-left. Spring easing on entry (0.34, 1.56, 0.64, 1), smooth ease-out on exit. Cursor switches between pointer (advance zone) and default (close zone).

State: Single currentIndex drives both stack and lightbox. Module-level savedIndex persists position across client-side navigation, resets on hard refresh. No configuration props beyond notes.

Visual Design

Font: Permanent Marker (Google Fonts) -- consumer must provide --font-marker CSS variable. The experiment page loads it directly; the blog loads it via app/(blog)/layout.tsx. Do NOT use @import url() in CSS modules -- Turbopack strips them.

Post-it palette:

VariantBackgroundTextBorder
warm#f5e960rgba(0,0,0,0.7)#e0d44e
cool#a8d8eargba(0,0,0,0.65)#94c4d6
neutral#f5c6d0rgba(0,0,0,0.65)#e0b2bc

Animations: CSS only, no framer-motion. Spring-like cubic-bezier (0.34, 1.56, 0.64, 1) for hover lift and lightbox entrance. Full-viewport slide transitions (translateX(100vw)) for note advancement, bidirectional.

  • app/design-experiments/sticky-notes/ -- component, loader, types, showcase page
  • app/(blog)/layout.tsx -- loads Permanent Marker font via next/font/google
  • app/(blog)/blog/page.tsx -- imports component, passes app/(blog)/notes as content source
  • app/(blog)/notes/*.md -- blog's note content