---
title: "The Prompt I Used to Build Pin Board, a Single-File Sticky-Notes App (No Backend, No Build Step)"
description: "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."
date: "2026-07-30"
type: "prompt"
tags: []
legacy: true
---
<h2>Overview</h2>
      <p>
        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.
      </p>
      <p>
        It is one <code>index.html</code> file. No backend, no build step, no
        framework — vanilla JS and <code>localStorage</code>. 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 <code>node_modules</code> folder and
        a dev server for something that should be a file you can email to
        someone.
      </p>
      <p>
        The prompt below is long, and the length is doing a specific job. Sticky
        notes are deceptively hard — not the rendering, the
        <em>state</em>. Most of the clauses exist to stop the app quietly losing
        your data.
      </p>

      <div class="highlight">
        <p>
          <strong>What it produces:</strong> 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.
        </p>
        <p style="margin-bottom: 0">
          <strong>Stack:</strong> nothing. One HTML file, inline CSS, vanilla
          JS, two Google Fonts. Open it with a double-click and it works.
        </p>
      </div>

      <h2>The prompt</h2>
      <p>
        Paste the whole thing at once.
      </p>

      <div class="prompt">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.</div>

      <h2>Why the prompt is written this way</h2>
      <p>
        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.
      </p>

      <h3>Enumerate what gets persisted</h3>
      <p>
        The single most valuable line in the prompt:
        <em>positions, sizes, rotation, z-index, colour, font, mode, text and
        checklist items are all persisted</em>.
      </p>
      <p>
        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.
      </p>

      <h3>Drag by the title bar, not the card</h3>
      <p>
        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.
      </p>
      <p>
        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.
      </p>

      <h3>Bounds clamping, because positions persist</h3>
      <p>
        <em>Notes stay within the viewport bounds; the board handles window
        resize without losing notes offscreen.</em>
      </p>
      <p>
        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
        <strong>permanent</strong>. The position gets written to
        <code>localStorage</code>, and now there is a note at
        <code>x: 2400</code> 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.
      </p>
      <p>
        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.
      </p>

      <h3>The checklist keyboard behaviour is the whole feature</h3>
      <p>
        Anyone can render a checkbox next to a line of text. What makes a
        checklist feel like a checklist is that
        <strong>Enter creates the next item</strong> and
        <strong>Backspace on an empty item deletes it and focuses the
        previous one</strong>.
      </p>
      <p>
        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.
      </p>

      <h3>Preserve the text when toggling modes</h3>
      <p>
        <em>Toggling back to plain text mode preserves the text as lines.</em>
      </p>
      <p>
        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.
      </p>
      <p>
        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.
      </p>

      <h3>Seed the empty state</h3>
      <p>
        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.
      </p>
      <p>
        It is also the difference between a screenshot worth posting and an
        empty rectangle.
      </p>

      <h2>The physicality is three details</h2>
      <p>
        The board reads as physical for cheaper reasons than you would expect,
        and they are all in the prompt:
      </p>
      <ul>
        <li>
          <strong>Random tilt per note (-3° to 3°).</strong> Perfectly aligned
          rectangles read as a grid. A degree or two of scatter reads as objects
          someone put down.
        </li>
        <li>
          <strong>Rotation straightens to 0° while dragging.</strong> 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.
        </li>
        <li>
          <strong>Layered shadow that deepens on lift.</strong> Depth cues that
          change with state are what make an element feel like it is
          <em>above</em> the board rather than drawn on it.
        </li>
      </ul>
      <p>
        The dot grid, vignette and paper grain are set dressing. The tilt is the
        illusion.
      </p>

      <h2>Knobs to turn first</h2>
      <ul>
        <li>
          <strong>Rotation range (±3°).</strong> The most sensitive number in
          the file. Past about 5° it stops reading as casual and starts reading
          as broken alignment.
        </li>
        <li>
          <strong>Dot grid opacity and spacing (26px).</strong> 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.
        </li>
        <li>
          <strong>Board colour (<code>#d8d2bd</code>).</strong> 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.
        </li>
        <li>
          <strong>Default note size (300×300).</strong> Square is a deliberate
          choice — it looks like a sticky note. Wider and it starts reading as a
          card or a window.
        </li>
        <li>
          <strong>The six paper colours.</strong> 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.
        </li>
      </ul>

      <h2>If it comes out wrong</h2>
      <ul>
        <li>
          <strong>Notes reset position on refresh.</strong> Only the text is
          being persisted. This is the default failure and the reason the prompt
          enumerates every field.
        </li>
        <li>
          <strong>You cannot select text inside a note.</strong> The drag
          handler is bound to the whole card instead of the title bar.
        </li>
        <li>
          <strong>A note has vanished and will not come back.</strong> Bounds
          clamping is missing and an offscreen position got saved. Clear
          <code>localStorage</code> to recover, then fix the clamp — including
          on <code>resize</code>, not just on drop.
        </li>
        <li>
          <strong>Switching to checklist and back loses the text.</strong> The
          two modes are stored as separate fields with no conversion between
          them.
        </li>
        <li>
          <strong>Backspace on an empty checklist item does nothing.</strong>
          The half of the keyboard contract that always gets dropped.
        </li>
        <li>
          <strong>Dragging is janky with a dozen notes on the board.</strong>
          The app is re-rendering every note on each <code>pointermove</code>.
          Move only the dragged element, and write to
          <code>localStorage</code> on drop rather than on every frame — a
          synchronous <code>localStorage</code> write per mouse move will stutter
          on its own.
        </li>
        <li>
          <strong>The board is blank and the console shows a JSON error.</strong>
          Corrupt or half-written saved state. Wrap the load in a
          <code>try/catch</code> that falls back to the seed notes instead of
          throwing, or one bad write bricks the app permanently.
        </li>
        <li>
          <strong>Clicking a note does not bring it forward.</strong> 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.
        </li>
      </ul>

      <h2>Recap</h2>
      <ul>
        <li>
          Say "one HTML file, no build step" or you will get a React project for
          something that should be a single file.
        </li>
        <li>
          Enumerate every persisted field. "Persists to localStorage" gets you
          the text and nothing else.
        </li>
        <li>
          Give dragging its own surface, or it fights text selection in the
          body.
        </li>
        <li>
          Persistence plus free positioning creates a data-loss bug neither
          feature has alone. Clamp to bounds, and re-clamp on resize.
        </li>
        <li>
          For checklists, specify Enter <em>and</em> Backspace. The second one
          is what makes it usable.
        </li>
        <li>
          Seed the empty state. A blank first load looks identical to a broken
          app.
        </li>
        <li>
          Straightening the tilt while dragging does more for physicality than
          any amount of shadow work.
        </li>
      </ul>

      <p>
        If you want more prompts in this shape, there is
        <a href="/blog/prompt-fluid-ink-reveal-wordmark-higgsfield"
          >a fluid ink-reveal homepage</a>,
        <a href="/blog/prompt-frosted-glass-refraction-cube-webgl"
          >a frosted-glass refraction cube in raw WebGL</a>, and
        <a href="/blog/wind-chime-effect">a wind chime app in one HTML file</a>.
      </p>
