Interaction Choreography

Sticky Note Board

Colourful sticky notes on a cork board: drag, add, and crumple to delete.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

A cork board rendered with CSS texture, covered in colourful sticky notes with slight rotation and tape / pushpin decorations: drag them freely, double-click to edit the text, press ✕ to crumple one into a paper ball and toss it away, and add a new auto-coloured note with one click in the bottom right — good for to-do walls, brainstorming boards, and other playful scenarios.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/sticky-note-board.en.json?t=<install token>"

New project or installation trouble? Start here →

Sticky Note Board is a React sticky notes board component built with Motion and Tailwind CSS. Notes can be dragged, edited with a double-click, added, and removed, and every change comes back as the full array through onChange so you can persist it yourself.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

300
Active edited usageDefault props
<StickyNoteBoard/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • Brainstorming boards and retrospective boards in team tools
  • A to-do or memo area on a personal dashboard
  • Interactive message walls on event pages or course platforms
  • A hands-on interactive section in a product tour or portfolio
  • Not a good fit: workflows that need grouped, ordered columns, such as a task board split by status; use a dedicated kanban or list component instead

Installation

npx webberui@latest add "https://webberui.com/r/sticky-note-board.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/sticky-note-board.

Usage

import { StickyNoteBoard, type StickyNote } from "@/components/ui/sticky-note-board";

const notes: StickyNote[] = [
  { id: "milk", text: "Buy milk after work 🥛", x: 24, y: 22, color: 0 },
  { id: "gym", text: "Gym Wed 19:00", x: 200, y: 120, color: 2 },
];

<StickyNoteBoard notes={notes} height={400} onChange={(next) => console.log(next)} />

Examples

Save to localStorage

notes is only the initial data, so read the saved content on the client first and then render the board; write it back on every onChange.

"use client";

import * as React from "react";
import { StickyNoteBoard, type StickyNote } from "@/components/ui/sticky-note-board";

const KEY = "my-board-notes";

export function PersistentBoard() {
  const [initial, setInitial] = React.useState<StickyNote[] | null>(null);

  React.useEffect(() => {
    const saved = localStorage.getItem(KEY);
    setInitial(saved ? (JSON.parse(saved) as StickyNote[]) : []);
  }, []);

  if (!initial) return null;

  return (
    <StickyNoteBoard
      notes={initial}
      onChange={(next) => localStorage.setItem(KEY, JSON.stringify(next))}
    />
  );
}

Sync to a backend

Drag end, committed edits, adds, and removals all fire onChange, so you can send the latest array straight to your API.

<StickyNoteBoard
  notes={notesFromServer}
  height={480}
  addLabel="Add idea"
  onChange={(next) => {
    void fetch("/api/boards/retro-42", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ notes: next }),
    });
  }}
/>

Fixed colours, no add button

Set a palette index with color (0–5 are yellow, pink, green, blue, orange, purple) and hide the add button with showAdd={false} so users only rearrange the existing notes.

const notes: StickyNote[] = [
  { id: "bug", text: "Login page breaks in Safari", x: 24, y: 24, color: 1 },
  { id: "idea", text: "Add a progress bar to checkout", x: 200, y: 40, color: 2 },
  { id: "todo", text: "Update the docs by Friday", x: 110, y: 170, color: 3 },
];

<StickyNoteBoard notes={notes} showAdd={false} height={320} />

Props

PropTypeDefaultDescription
notesStickyNote[][]Initial note data; from then on the component manages it internally and reports changes through onChange
onChange(notes: StickyNote[]) => void—Returns the latest note array after any drag, edit, addition, or crumple
heightnumber380Cork board height (px)
addLabelstring"Add a sticky note"Text of the add button
showAddbooleantrueWhether to show the add button in the bottom right
classNamestring—Forwarded to the outermost (wooden frame) container

StickyNote type

FieldTypeDescription
idstringUnique identifier, also the deterministic hash seed for the rotation angle and the pushpin / tape style
textstringText content on the note
xnumberX coordinate relative to the board's top-left corner (px)
ynumberY coordinate relative to the board's top-left corner (px)
colornumberSwatch index 0–5 (yellow/pink/green/blue/orange/purple); when unspecified, the colour is derived from the id hash

Accessibility

  • Every note is keyboard-focusable: Enter enters edit mode, arrow keys move it (Shift accelerates), Delete / Backspace crumples and deletes it
  • Notes use role="group" with aria-label to read out their content, and the crumple button has an explicit aria-label
  • While editing, Esc cancels and restores the original text, and Cmd/Ctrl + Enter or blurring submits
  • When the user has "reduce motion" enabled at the system level, the reveal, pick-up scaling, and paper-crumple animations all become plain fade in / fade out
  • The rotation angle and decoration style are produced by a deterministic hash of the id, so SSR and client rendering match

FAQ

Does it work with the Next.js App Router?

Yes. The file already starts with "use client", so you can import it from a Server Component page as usual. Each note's rotation and decoration come from a deterministic hash of its id, so server and client renders match.

Why doesn't the board update when I change notes?

notes is read only on the first render; after that the component manages its own state. To swap in entirely new data, change the component's key so it remounts.

How do I store note positions?

Each note's x and y are pixel coordinates relative to the board's top-left corner. After a drag or an arrow-key move they are rounded to integers and returned through onChange. Save the returned array as-is and pass it back as notes to restore the board.

Can it be used with a keyboard?

Yes. Every note is focusable: Enter edits, arrow keys move it (hold Shift for bigger steps), and Delete or Backspace removes it. While editing, Esc cancels and Cmd/Ctrl + Enter commits.

What are the dependencies?

motion, lucide-react, clsx, and tailwind-merge. The cork texture is pure CSS gradients, so no image files are needed.