Position floating sidenotes/comments next to a document with inline references.
- Place notes/comments to the side of a document with inline references.
- When an inline reference is clicked, animate the relevant sidenote to be as close as possible and move non-relevant sidenotes out of the way without overlapping.
- Do not provide UI or impose any styling, only placement.
- Comment streams next to a document. This is showing Curvenote, which is a scientific writing platform that connects to Jupyter.

- React 18/19 with
useReducer+ Context (no Redux dependency) - TypeScript 6
- Vite 8 for the demo,
tscfor the library - Bun as the package manager
See demo/index.tsx for the full example.
bun install
bun run devbun add sidenotes
# or: npm install sidenotesWrap the content that contains sidenotes in a <SidenotesProvider>. Put inline references inside <InlineAnchor> and the floating sidenote cards inside <Sidenote>. <AnchorBase> is an optional fallback target used when no inline anchor is mounted.
import {
SidenotesProvider,
Sidenote,
InlineAnchor,
AnchorBase,
useSidenotes,
} from 'sidenotes';
function Doc() {
const { deselect } = useSidenotes();
return (
<article onClick={deselect}>
<AnchorBase anchor="anchor">
Content with <InlineAnchor sidenote="note-1">an inline reference</InlineAnchor>.
</AnchorBase>
<div className="sidenotes">
<Sidenote sidenote="note-1" base="anchor">
Your custom UI, e.g. a comment.
</Sidenote>
</div>
</article>
);
}
export default function App() {
return (
<SidenotesProvider padding={10}>
<Doc />
</SidenotesProvider>
);
}useSidenotes() is the public surface for interacting with sidenotes imperatively. It is backed by a stable control context and does not re-render when the selection changes — read the current selection by calling the getter functions.
const {
getSelectedSidenote, // () => string | null
getSelectedAnchor, // () => string | null
selectSidenote, // (sidenoteId: string) => void
selectAnchor, // (anchor: string | HTMLElement) => void
deselect, // () => void
reposition, // () => void — recompute positions (e.g. after layout change)
} = useSidenotes();If you need to re-render a component when the selection changes, read the state directly from context — the getters are intentionally decoupled from React's re-render loop.
Everything else (reducer, action creators, selectors, dispatch) is internal.
The library does not ship any CSS. Components render with stable class names so you can style them however you want:
| Component | Element / class |
|---|---|
InlineAnchor |
<span class="anchor [selected]"> |
AnchorBase |
<div class="[selected]"> |
Sidenote |
<div class="sidenote [selected]"> |
| Container | whatever wraps your <Sidenote> list (e.g. <div className="sidenotes">) |
The demo's demo/index.css has a full working Tailwind v4 setup you can copy as a starting point.
- Sidenotes positioning is computed relative to a wrapping
<article>element. - Each
<SidenotesProvider>owns one document. Use multiple providers if you need more than one. InlineAnchorrenders a<span>;AnchorBaserenders a<div>;Sidenoterenders a<div>.
bun install
bun run dev # demo with HMR at http://localhost:3013
bun run build # type-check + emit library to dist/
bun run build:demo # build the demo site to dist-demo/
bun run typecheck
bun run lint
bun run format
bun run render-check # build the demo and assert React output in happy-dom- Better mobile layout that places notes at the bottom.
