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--StickyNoteinterface (id, date, color, content)app/design-experiments/sticky-notes/loadNotes.ts--getAllNotes(notesDir), accepts path to notes directoryapp/design-experiments/sticky-notes/components/StickyNoteStack.tsx-- client component, all interaction logicapp/design-experiments/sticky-notes/components/stickyNotes.module.css-- all styling including animationsapp/design-experiments/sticky-notes/index.ts-- barrel exportapp/design-experiments/sticky-notes/page.tsx-- experiment showcase pageapp/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:
| Variant | Background | Text | Border |
|---|---|---|---|
| warm | #f5e960 | rgba(0,0,0,0.7) | #e0d44e |
| cool | #a8d8ea | rgba(0,0,0,0.65) | #94c4d6 |
| neutral | #f5c6d0 | rgba(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.
Related Files
app/design-experiments/sticky-notes/-- component, loader, types, showcase pageapp/(blog)/layout.tsx-- loads Permanent Marker font vianext/font/googleapp/(blog)/blog/page.tsx-- imports component, passesapp/(blog)/notesas content sourceapp/(blog)/notes/*.md-- blog's note content