Sticky notes for any React app
A shadcn registry: a composable sticky note, a full board, and the hooks underneath, after Murmur's notes. Notes copy and paste in the Stickies format, so they move between any apps that use it, and still paste as coloured boxes or plain text everywhere else.
Install
Needs a shadcn project with Tailwind v4 and React 19. Add the whole board, and everything it uses comes with it:
npx shadcn@latest add https://stickies.live/r/sticky-board.jsonOr add the namespace to components.json once, and use short names:
{
"registries": {
"@stickies": "https://stickies.live/r/{name}.json"
}
}npx shadcn@latest add @stickies/sticky-note @stickies/use-sticky-notesSticky board
A canvas of notes. Drag a note by its top, resize from the corner, drag the background to look around, double-click to add a note. Click to select, ⇧-click for more, ⌘A for all. ⌘C, ⌘X and ⌘V copy, cut and paste in the Stickies format, and Delete removes. It fills its parent and saves to localStorage by default.
npx shadcn@latest add https://stickies.live/r/sticky-board.jsonimport { StickyBoard, useStickyBoard } from "@/components/sticky-board"
import { createNote } from "@/lib/stickies-format"
const notes = [createNote({ text: "Hello", x: 0, y: 0 })]
function Toolbar() {
const { selectedNotes, copy, reset } = useStickyBoard()
return <button onClick={() => copy(selectedNotes)}>Copy</button>
}
export function Board() {
return (
<div className="h-[600px]">
<StickyBoard defaultNotes={notes} storageKey="my-board" source={{ app: "My app" }}>
<Toolbar />
</StickyBoard>
</div>
)
}| Prop | Type | What it does |
|---|---|---|
| defaultNotes | StickyNoteData[] | Notes to start with, and to go back to on reset(). |
| notes / onNotesChange | StickyNoteData[] | Controlled notes. Nothing is saved to the browser. |
| persist | boolean = true | Keep the board in localStorage, as a Stickies document. |
| storageKey | string | Where in localStorage. Give each board its own. |
| semantic | boolean = false | Colour comes from state: open is yellow, in progress blue, done green. |
| showDate | boolean = false | Show when each note last changed. |
| source | StickiesSource | Recorded in copied notes, so other apps know where they came from. |
| renderNote | (note, { autoFocus }) => ReactNode | Your own composition of a note's parts. |
| children | ReactNode | Drawn over the board. Use useStickyBoard() inside. |
Sticky note
The note on its own, in parts. Leave any out: without StickyNoteResize it doesn't resize, without StickyNoteColors it keeps its colour. With positioned={false} it sits in a list or grid like any block.
Default parts
semantic, with StickyNoteDate
npx shadcn@latest add https://stickies.live/r/sticky-note.json<StickyNote note={note} onNoteChange={setNote} onDelete={remove}>
<StickyNoteHeader>
<StickyNoteColors />
<span className="flex-1" />
<StickyNoteState />
<StickyNoteClose />
</StickyNoteHeader>
<StickyNoteText />
<StickyNoteFooter>
<StickyNoteAuthor />
<StickyNoteDate />
</StickyNoteFooter>
<StickyNoteResize />
</StickyNote>| Prop | Type | What it does |
|---|---|---|
| StickyNote | note, onNoteChange, onDelete? | The paper. Also positioned, semantic, scale and selected. Gives its parts the note. |
| StickyNoteHeader | div | The handle on a canvas: drag it, or focus it and use the arrow keys (⇧ for bigger steps). |
| StickyNoteColors | div | The colour picker, on hover or focus. With semantic, it picks the state instead. |
| StickyNoteState | span | A mark for in progress or done, so state never rests on colour alone. |
| StickyNoteClose | button | Delete. A note with words asks first, in the footer. |
| StickyNoteText | textarea | The words, with lists (lists={false} to turn off). Escape returns focus to the header. |
| StickyNoteFooter | div | For the author and date; where deleting asks first. |
| StickyNoteAuthor / StickyNoteDate | span / time | From the note's author and updatedAt. |
| StickyNoteResize | div | The bottom-right corner. |
| useStickyNote() | hook | The note and its edit functions, for your own parts. |
useStickyNotes
The board's state without the board: add, update, remove, raise, select, reset. Saved to localStorage unless you pass persist: false or control the notes yourself. The array's order is the stack.
npx shadcn@latest add https://stickies.live/r/use-sticky-notes.jsonconst { notes, ready, add, update, remove, raise, selected, select, reset } =
useStickyNotes({ defaultNotes, storageKey: "my-board" })
add({ text: "New", x: 40, y: 40 })
update(id, { state: "done" }) // sets updatedAt
update(id, { x: 100 }, { touch: false }) // geometry onlyuseStickiesClipboard
⌘C, ⌘X and ⌘V in the Stickies format. Copying writes the Stickies JSON, HTML that carries the document, and plain text, so notes paste into docs and email as coloured boxes and into Stickies apps whole. Pasting reads the richest version there. Typing in a field is left alone.
npx shadcn@latest add https://stickies.live/r/use-stickies-clipboard.jsonconst { copy } = useStickiesClipboard({
getNotes: () => selectedNotes,
onPaste: ({ notes }) => insert(placeNotes(notes, { x: 40, y: 40 })),
onCut: (notes) => remove(notes.map((n) => n.id)),
source: { app: "My app" },
})
// From a button: HTML and plain text only, as browsers allow
await copy(selectedNotes)Safari removes custom clipboard types between sites, so the HTML's data-stickies is what carries notes there. See §8.5 of the spec for what each browser allows.
usePointerDrag
Moving or resizing anything with the pointer, captured so it carries on outside the element, and divided by the zoom.
npx shadcn@latest add https://stickies.live/r/use-pointer-drag.jsonconst start = useRef(position)
const { handlers, dragging } = usePointerDrag({
onDragStart: () => { start.current = position },
onDrag: ({ dx, dy }) => setPosition({ x: start.current.x + dx, y: start.current.y + dy }),
})
return <div {...handlers} />useCanvasPan
Dragging a surface to look around it. Only presses on the surface itself pan, so what's on it keeps its own dragging.
npx shadcn@latest add https://stickies.live/r/use-canvas-pan.jsonconst { pan, handlers, toCanvas } = useCanvasPan()
return (
<div {...handlers} className="relative h-full overflow-hidden">
<div style={{ transform: `translate(${pan.x}px, ${pan.y}px)` }}>{children}</div>
</div>
)useListFormatting
Lists in a plain textarea, as in Apple's Notes: "- " starts a bullet, Return carries the list on, and Return on an empty item ends it. Numbered lists keep counting.
npx shadcn@latest add https://stickies.live/r/use-list-formatting.jsonconst list = useListFormatting(text, setText)
return <textarea {...list} />Stickies format
The spec in TypeScript, with no dependencies. Everything else here uses it.
npx shadcn@latest add https://stickies.live/r/stickies-format.json| Prop | Type | What it does |
|---|---|---|
| createNote(fields) | StickyNoteData | A new note with an ID, size, colour and times. |
| changeNote(note, fields) | StickyNoteData | A changed note, with updatedAt set and color kept in step with colorName. |
| toDocument(notes, { source }) | StickiesDocument | Notes as a document, also valid JSON Canvas: save it as .canvas for Obsidian. |
| parseDocument(json) / validate(value) | document | null / problems | Reading and checking against the spec. |
| toHtml(doc) / fromHtml(html) | string / document | null | The clipboard's HTML version, with the document in data-stickies. |
| toText(notes) / fromText(text) | string / notes | Plain text, one note per paragraph. |
| placeNotes(notes, at) | StickyNoteData[] | Pasted notes with new IDs, moved together to a point. |
| stateOf, colorOf, shownColorOf | Reading a note as the spec says: unknown states are open, unknown colours yellow. |
Theme
The six colours as variables on [data-sticky-color], light and dark (shadcn's .dark), the sticky-paper, sticky-header, sticky-edge, sticky-ink and sticky-meta colours for Tailwind, and the sticky-shadow utility. Change the variables in your CSS to change the palette.
npx shadcn@latest add https://stickies.live/r/stickies-theme.json<div data-sticky-color="blue" className="bg-sticky-paper text-sticky-ink sticky-shadow" />For agents
llms.txt sums up the format. The Stickies skill lets an agent make notes, check them, and put them on the clipboard in every format, ready to paste onto a board like this one.