← All @molecule/* packages · App templates
@molecule/app-margin-notes-reactFeature · margin-notes · App (browser) · v1.0.1 · Apache-2.0
Long-form reading layout with notes beside each paragraph: aligned notes column, margin marks, two-way hover, switchable note kinds, and a phone bar that follows the reader
npm install @molecule/app-margin-notes-react@molecule/app-margin-notes-react is a ready-made margin-notes feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.
import { MarginNotes } from '@molecule/app-margin-notes-react'
// blocks: your rendered paragraphs, in order; notes: what goes beside them.
;<MarginNotes
blocks={[
{ id: 'p1', content: <p>Every post carries a map of who wrote what.</p>, noteIds: ['s1'] },
{
id: 'p2',
content: <p>The map is built from the session export.</p>,
noteIds: ['s1', 'q1'],
marked: true,
},
]}
notes={[
{ id: 's1', kind: 'summary', content: <p>How the map is made.</p> },
{ id: 'q1', kind: 'prompt', content: <p>“explain the map” — claude-opus-4</p> },
]}
kinds={[
{
id: 'summary',
label: t('post.summaries', undefined, { defaultValue: 'Summaries' }),
defaultOn: true,
},
{
id: 'prompt',
label: t('post.prompts', undefined, { defaultValue: 'Prompts' }),
defaultOn: false,
panel: 'tap',
},
]}
markLabel={t('post.aiWritten', undefined, { defaultValue: 'Written with AI' })}
/>Works with: @molecule/app-react, @molecule/app-ui, @molecule/app-ui-react
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Margin notes: long-form text with notes beside each paragraph — annotated essays, commentary, and posts that show which paragraphs an AI wrote and the prompt behind each.
Desktop: a large-type prose column and a narrower notes column to its right; each note lines up with the paragraph (or run of paragraphs) it belongs to and appears once. Hovering or focusing a paragraph emphasises its notes; hovering or focusing a note tints the paragraphs it covers. A paragraph can carry a small mark in its left margin. Note kinds (say, "Summaries" and "Prompts") each get a real switch. Phone: the notes column gives way to a bar fixed to the bottom that shows the notes for the section being read and follows the reader; tapping a paragraph pins its notes, tapping again lets go; the switches live in the bar. A document with no notes renders centred prose at full measure with no notes column.
import { MarginNotes } from '@molecule/app-margin-notes-react'
// blocks: your rendered paragraphs, in order; notes: what goes beside them.
;<MarginNotes
blocks={[
{ id: 'p1', content: <p>Every post carries a map of who wrote what.</p>, noteIds: ['s1'] },
{
id: 'p2',
content: <p>The map is built from the session export.</p>,
noteIds: ['s1', 'q1'],
marked: true,
},
]}
notes={[
{ id: 's1', kind: 'summary', content: <p>How the map is made.</p> },
{ id: 'q1', kind: 'prompt', content: <p>“explain the map” — claude-opus-4</p> },
]}
kinds={[
{
id: 'summary',
label: t('post.summaries', undefined, { defaultValue: 'Summaries' }),
defaultOn: true,
},
{
id: 'prompt',
label: t('post.prompts', undefined, { defaultValue: 'Prompts' }),
defaultOn: false,
panel: 'tap',
},
]}
markLabel={t('post.aiWritten', undefined, { defaultValue: 'Written with AI' })}
/>
feature
npm install @molecule/app-margin-notes-react @molecule/app-react @molecule/app-ui @molecule/app-ui-react react
npm install -D @types/react
MarginNoteA note shown beside the block(s) it belongs to.
interface MarginNote {
/** Stable id, referenced from {@link MarginNotesBlock.noteIds}. */
id: string
/** The kind this note belongs to ({@link MarginNoteKind.id}), for its switch. */
kind: string
/** The rendered note. */
content: ReactNode
}
MarginNoteKindA kind of note the reader can show or hide with a switch.
interface MarginNoteKind {
/** Stable id, referenced from {@link MarginNote.kind}. */
id: string
/** The switch's visible label, already translated (e.g. "Summaries"). */
label: string
/** Whether this kind shows before the reader touches its switch. Default `true`. */
defaultOn?: boolean
/**
* How this kind reaches the phone panel. `'follow'` (default): the panel
* shows it for the section being read, following the reader. `'tap'`: only
* when the reader taps a block it belongs to (and gone on the second tap) —
* for notes about one paragraph rather than the section.
*/
panel?: 'follow' | 'tap'
}
MarginNotesBlockOne block of the text — usually a paragraph or heading from your markdown renderer. Blocks render in order, in the prose column.
interface MarginNotesBlock {
/** Stable id, unique within the document (used as the DOM anchor). */
id: string
/** The rendered block (e.g. `<p>…</p>`). */
content: ReactNode
/** Ids of the notes that belong to this block (see {@link MarginNote}). */
noteIds?: string[]
/**
* Draw a small mark in this block's left margin — the one permanent sign on
* the prose itself (e.g. "written with AI"). Unmarked blocks carry none.
*/
marked?: boolean
}
MarginNotesProps<MarginNotes> props.
interface MarginNotesProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
/** The text, in reading order. */
blocks: MarginNotesBlock[]
/** Every note the blocks refer to. A document with none renders centred prose with no gutter. */
notes?: MarginNote[]
/** The note kinds, in the order their switches appear. Kinds absent here are always shown. */
kinds?: MarginNoteKind[]
/** Accessible name of the margin mark, already translated (e.g. "Written with AI"). */
markLabel?: string
/** Width of the prose column (a CSS length). Default `38rem`. */
measure?: string
/** Width of the notes column (a CSS length). Default `18rem`. */
gutterWidth?: string
/** Controlled: the ids of the kinds currently shown. Omit to let the component own it. */
shownKinds?: string[]
/** Called with the new list of shown kind ids whenever the reader flips a switch. */
onShownKindsChange?: (shown: string[]) => void
}
MarginNotesRowOne row of the layout: a run of blocks and the notes shown beside it.
interface MarginNotesRow {
/** The row's key: the id of its first block. */
id: string
/** The blocks in this row, in order. */
blocks: MarginNotesBlock[]
/** Notes shown beside this row — each note appears in exactly one row, its first. */
notes: MarginNote[]
/** Every note id any block in this row refers to (for the phone panel). */
noteIds: string[]
}
buildRows(blocks, notes)Group blocks into layout rows and place each note once.
A row is a run of consecutive blocks that refer to exactly the same set of notes (a paragraph run sharing one note), or a run of consecutive blocks with no notes. Each note is shown beside the FIRST row that refers to it and never again, so a note covering several paragraphs — even ones that are not next to each other — appears once on the page.
function buildRows(blocks: MarginNotesBlock[], notes?: MarginNote[]): MarginNotesRow[]
blocks — The text, in reading order.notes — Every note the blocks refer to; ids with no note are ignored.Returns: The rows, in reading order.
defaultShownKinds(kinds)The kinds shown before the reader touches a switch.
function defaultShownKinds(kinds?: { id: string; defaultOn?: boolean }[]): string[]
kinds — The switchable kinds.Returns: The ids of the kinds that default on.
MarginNotes(props)Long-form text with notes beside each paragraph.
Desktop: a prose column and, to its right, a narrower notes column; each note lines up with the block(s) it belongs to and appears once. Phone: the notes column disappears and a bar fixed to the bottom shows the notes for the section being read (tap a block to pin its notes, tap again to let go), with the kind switches in the bar. Every default — which kinds show, which surface a width gets — is decided in the first render and by CSS, so server-rendered or prerendered HTML is right before any JavaScript runs.
function MarginNotes(props: MarginNotesProps): JSX.Element
props — See {@link MarginNotesProps}.Returns: The layout.
Peer dependencies:
@molecule/app-react ^1.0.1@molecule/app-ui ^1.2.0@molecule/app-ui-react ^1.0.1react ^18.0.0 || ^19.0.0@molecule/app-react
@molecule/app-ui
@molecule/app-ui-react
react
Defaults are right before JavaScript runs. Which kinds show comes from
kinds[].defaultOn in the FIRST render (a hidden note carries the HTML
hidden attribute), and the desktop/phone split is CSS
(cm.hiddenBelow('md') / cm.hiddenFrom('md')). Prerender the page with
renderToString and a kind that defaults off is already absent; do NOT
toggle defaults in a useEffect of your own.
One note, one place. A note shows beside the FIRST row that refers to
it. Give consecutive paragraphs that share a note the same noteIds and
they form one row with the note beside them once. A paragraph whose
noteIds differ from its neighbour's starts a new row.
The mark is drawn on the block, as an absolutely-placed dot inside the block's own wrapper (hidden on phones, where there is no margin). At rest a marked block is typeset exactly like an unmarked one.
Pass rendered blocks, not markdown. Split your renderer's output into
top-level blocks (one per paragraph/heading/list) and give each a stable
id; the component adds no markup inside them.
Section notes follow the reader; paragraph notes wait for a tap. On a
phone the panel shows the kinds with panel: 'follow' (the default) for
the section in view. Give a kind that is about ONE paragraph (a prompt, a
citation) panel: 'tap': it shows when that paragraph is tapped and goes
on the second tap.
Controlled or not. Pass shownKinds + onShownKindsChange to own the
switches' state (e.g. to remember it); otherwise the component does.
The phone bar reserves space at the end of the text so it never covers the
last paragraph. UI strings come from the companion locale bond
@molecule/app-locales-margin-notes; kind labels and markLabel are yours
to translate.
Translation strings are provided by @molecule/app-locales-margin-notes.