Overview
A kraft-paper board covered in sticky notes you can drag around, resize, recolour, and flip between free text and a checklist. Each note has a folded corner, a slight random tilt, and straightens itself as you pick it up. Everything survives a refresh.
It is one index.html file. No backend, no build step, no
framework — vanilla JS and localStorage. That constraint is
not decoration: a note-taking app is exactly the kind of thing a model
will reflexively scaffold as a React project with a store and a
component tree, and then you have a node_modules folder and
a dev server for something that should be a file you can email to
someone.
The prompt below is long, and the length is doing a specific job. Sticky notes are deceptively hard — not the rendering, the state. Most of the clauses exist to stop the app quietly losing your data.
What it produces: full-viewport tan pinboard with a dot grid and paper grain, draggable and resizable notes in six colours, a per-note settings strip for paper / typeface / checklist mode, a live counter of open tasks, and a bottom toolbar for spawning notes.
Stack: nothing. One HTML file, inline CSS, vanilla JS, two Google Fonts. Open it with a double-click and it works.
The prompt
Paste the whole thing at once.
Why the prompt is written this way
Strip this down to "build a sticky notes app with localStorage" and you get something that demos fine for thirty seconds and then loses your work. Six clauses are carrying the result.
Enumerate what gets persisted
The single most valuable line in the prompt: positions, sizes, rotation, z-index, colour, font, mode, text and checklist items are all persisted.
Say only "persists to localStorage" and you will reliably get the note text saved and nothing else. Refresh, and every note snaps back to a default position in a default colour — which is worse than not persisting at all, because it looks like it worked until you notice the board has rearranged itself. Listing the fields turns a vague instruction into a schema.
Drag by the title bar, not the card
This looks like a styling note and it is actually a conflict resolution. The body of a note is editable text, and text needs click-and-drag to select. Bind the drag handler to the whole card and those two gestures fight: you try to select a sentence and the note skates across the board.
Giving drag its own dedicated surface is why the title bar exists at all. The three decorative dots on the left are doing quiet work here too — they read as a grip, so the affordance is discoverable without a tooltip.
Bounds clamping, because positions persist
Notes stay within the viewport bounds; the board handles window resize without losing notes offscreen.
In an app with no persistence, dragging a note off the edge is a
temporary annoyance you fix by dragging it back. Here it is
permanent. The position gets written to
localStorage, and now there is a note at
x: 2400 that you cannot see, cannot reach, and cannot
delete — on a laptop screen it simply does not exist any more. Same
thing happens passively when someone drags the window narrower.
Combining persistence with free positioning creates a data-loss bug that neither feature has on its own. Clamping on drag and re-clamping on resize is the fix, and it has to be asked for.
The checklist keyboard behaviour is the whole feature
Anyone can render a checkbox next to a line of text. What makes a checklist feel like a checklist is that Enter creates the next item and Backspace on an empty item deletes it and focuses the previous one.
That second half is the one that gets skipped. Without it there is no keyboard way to back out of a list you over-created — you have to reach for the mouse to delete an empty row, which breaks the flow that made you use a checklist in the first place. It is one clause and it is the difference between a real list and a decorative one.
Preserve the text when toggling modes
Toggling back to plain text mode preserves the text as lines.
The naive implementation stores text and items as two separate fields and swaps which one renders. Flip to checklist, flip back, and your paragraph is gone — you have been shown an empty text area because the content lives in the other field now.
Requiring the round-trip forces a conversion between the two shapes: split on newlines going in, join on newlines coming out. It is a small thing to implement and an infuriating one to discover missing, because by then you have lost something you typed.
Seed the empty state
A blank board on first load is indistinguishable from a broken board. No notes, no visible controls doing anything, nothing to indicate the app works. Seeding a handful of mixed-colour notes means the first frame demonstrates dragging, colour, and checklist mode without a word of onboarding.
It is also the difference between a screenshot worth posting and an empty rectangle.
The physicality is three details
The board reads as physical for cheaper reasons than you would expect, and they are all in the prompt:
- Random tilt per note (-3° to 3°). Perfectly aligned rectangles read as a grid. A degree or two of scatter reads as objects someone put down.
- Rotation straightens to 0° while dragging. This is the one that sells it. Picking a note up squares it to the board and dropping it lets it settle back — the same thing your hand does with a real piece of paper. It costs one line in a transition and does more than any shadow.
- Layered shadow that deepens on lift. Depth cues that change with state are what make an element feel like it is above the board rather than drawn on it.
The dot grid, vignette and paper grain are set dressing. The tilt is the illusion.
Knobs to turn first
- Rotation range (±3°). The most sensitive number in the file. Past about 5° it stops reading as casual and starts reading as broken alignment.
- Dot grid opacity and spacing (26px). Easy to overdo. The grid should be barely perceptible — if you can count the dots without trying, it is too strong and it competes with the notes.
-
Board colour (
#d8d2bd). Everything else is tuned against this. Go much lighter and the cream note becomes invisible against it; go much darker and the whole thing turns muddy. - Default note size (300×300). Square is a deliberate choice — it looks like a sticky note. Wider and it starts reading as a card or a window.
- The six paper colours. Keep them all desaturated and in the same value range. One swatch brighter than the rest will dominate the board and you will unconsciously stop using the others.
If it comes out wrong
- Notes reset position on refresh. Only the text is being persisted. This is the default failure and the reason the prompt enumerates every field.
- You cannot select text inside a note. The drag handler is bound to the whole card instead of the title bar.
-
A note has vanished and will not come back. Bounds
clamping is missing and an offscreen position got saved. Clear
localStorageto recover, then fix the clamp — including onresize, not just on drop. - Switching to checklist and back loses the text. The two modes are stored as separate fields with no conversion between them.
- Backspace on an empty checklist item does nothing. The half of the keyboard contract that always gets dropped.
-
Dragging is janky with a dozen notes on the board.
The app is re-rendering every note on each
pointermove. Move only the dragged element, and write tolocalStorageon drop rather than on every frame — a synchronouslocalStoragewrite per mouse move will stutter on its own. -
The board is blank and the console shows a JSON error.
Corrupt or half-written saved state. Wrap the load in a
try/catchthat falls back to the seed notes instead of throwing, or one bad write bricks the app permanently. - Clicking a note does not bring it forward. z-index is being assigned at creation and never updated. It needs to be a counter that increments on click — and it needs persisting, or the stacking order shuffles on reload.
Recap
- Say "one HTML file, no build step" or you will get a React project for something that should be a single file.
- Enumerate every persisted field. "Persists to localStorage" gets you the text and nothing else.
- Give dragging its own surface, or it fights text selection in the body.
- Persistence plus free positioning creates a data-loss bug neither feature has alone. Clamp to bounds, and re-clamp on resize.
- For checklists, specify Enter and Backspace. The second one is what makes it usable.
- Seed the empty state. A blank first load looks identical to a broken app.
- Straightening the tilt while dragging does more for physicality than any amount of shadow work.
If you want more prompts in this shape, there is a fluid ink-reveal homepage, a frosted-glass refraction cube in raw WebGL, and a wind chime app in one HTML file.