← Learn · Prompt · 30 July 2026 · 10 min

The Prompt I Used to Build Pin Board, a Single-File Sticky-Notes App (No Backend, No Build Step)

A copy-pasteable prompt for a draggable, resizable sticky-notes board with per-note colour, font and checklist modes — one HTML file, vanilla JS, everything persisted to localStorage. Plus why each clause is in the prompt.

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.

Build a single-file, fully functional sticky-notes web app called "Pin Board". No backend, no build step — one HTML file with inline CSS and vanilla JS. All state persists to localStorage so notes survive refresh. CANVAS - Full-viewport board with a warm tan/kraft paper background (#d8d2bd-ish), a faint dot grid (~2px dots, 26px spacing, low opacity), plus a subtle radial vignette and paper-grain noise so it reads as a physical pinboard. - Top-left header: "Pin Board" in a handwriting script font (Caveat), and under it a small uppercase letterspaced mono subtitle: COLOUR · TYPE · CHECKLIST — PER NOTE - Top-right: live counter in the same mono style — "6 NOTES · 0 OPEN TASKS". Open tasks = unchecked checklist items across all notes. - Fonts from Google Fonts: Caveat (handwriting) and a mono like JetBrains Mono or IBM Plex Mono. NOTES Each note is an absolutely positioned card: - Square-ish by default (~300×300), a random slight rotation between -3° and 3°, and a layered drop shadow so it lifts off the board. - A darker shade of the note's own colour as a thin title bar, with three small dots on the left (decorative), and on the right two square buttons: ⋯ (settings) and × (delete, with a confirm-on-second-click or fade-out). - Bottom-right folded-corner triangle (CSS) suggesting a peeled sticky corner. - Body is an editable text area with placeholder text — "type something…" in mono, "Scribble something…" in handwriting — that grows with content. - Drag by the title bar to move. Bottom-right corner is a resize handle (min ~180×160). Clicking anywhere on a note raises it to the top of the z-order. - Positions, sizes, rotation, z-index, colour, font, mode, text and checklist items are ALL persisted. PER-NOTE SETTINGS PANEL Clicking ⋯ slides open a settings strip between the title bar and the body, separated by a dashed rule: - PAPER — six circular swatches: yellow, salmon/coral, green, blue, purple, cream. The active one gets a dark ring, and choosing one recolours the whole note including the title bar and folded corner. - TYPE — two pill buttons, "Handwriting" (rendered in Caveat) and "MONO" (rendered in mono). The active pill is filled dark with light text. - MODE — a single pill toggling "CHECKLIST OFF" / "CHECKLIST ON". In checklist mode the body becomes a list of items, each with a small square checkbox and inline-editable text. Enter creates the next item. Backspace on an empty item deletes it and focuses the previous one. Checked items get a strikethrough and dimmed text. Toggling back to plain text mode preserves the text as lines. BOTTOM TOOLBAR Fixed, centered near the bottom: a dark rounded pill containing "ADD A NOTE" in letterspaced mono, then the six colour swatches (clicking one spawns a new note of that colour at a slightly randomised position near the center, focused and ready to type), a thin vertical divider, and a "CLEAR ALL" button that asks for confirmation before wiping the board and localStorage. INTERACTION POLISH - Smooth spring-ish transitions on hover, note lift while dragging (scale up slightly, rotation straightens to 0°, shadow deepens), and settle back on drop. - Notes stay within the viewport bounds; the board handles window resize without losing notes offscreen. - Keyboard: N spawns a new note, Esc closes any open settings panel, Cmd/Ctrl+Z undoes the last delete. - Seed the empty state with a handful of notes in mixed colours so the board never opens blank on first visit. - Fully responsive; on small screens the toolbar wraps and notes clamp to available width.

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 localStorage to recover, then fix the clamp — including on resize, 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 to localStorage on drop rather than on every frame — a synchronous localStorage write 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/catch that 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.