In-Memory Versioning
This example shows how to use the VersioningExtension without any collaboration layer (no Yjs required). Snapshots are stored in memory using ProseMirror JSON.
The sidebar opens on a document with a few versions already in its history, so you can preview them, compare them, rename them, and restore them right away. The editor is read-only while the sidebar is open: close it to edit the document, then reopen it with the "History" button and press "Save version" to add a version of your own.
import "@blocknote/core/fonts/inter.css";import { BlockNoteEditor } from "@blocknote/core";import { VersioningExtension, createInMemoryVersioningAdapter,} from "@blocknote/core/extensions";import { DiffVersioningExtension } from "@blocknote/core/y";import { BlockNoteViewEditor, DefaultVersionMenuItems, useCreateBlockNote, useVersionSnapshot, VersionMenu, VersionMenuItem, VersioningSidebar,} from "@blocknote/react";import { RiFileCopyLine } from "react-icons/ri";import { BlockNoteView } from "@blocknote/mantine";import "@blocknote/mantine/style.css";import { useState } from "react";import { DAY_MS, LIVE_DOCUMENT, SAMPLE_HISTORY } from "./sampleVersions";import "./style.css";export default function App() { // The adapter is created per editor, so it's passed as a factory: the // VersioningExtension calls it with the editor instance once that's ready. // The store starts out with a few versions, the way an application would // load the history it persisted. const editor = useCreateBlockNote({ initialContent: LIVE_DOCUMENT, extensions: [ VersioningExtension((editor) => createInMemoryVersioningAdapter(editor, { initialVersions: SAMPLE_HISTORY.map((version) => ({ name: version.name, createdAt: Date.now() - version.daysAgo * DAY_MS, // The store keeps `Block[]`; a headless editor fills in the block // defaults the sample leaves out. content: BlockNoteEditor.create({ initialContent: version.blocks }) .document, })), }), ), // Opt into rendering version diffs: when comparing two versions the // sidebar shows insertions/deletions as attributed marks. Without this // extension the in-memory versioning falls back to a plain document swap. DiffVersioningExtension(), ], }); const [showSidebar, setShowSidebar] = useState(true); return ( <div className="wrapper"> {/* No `editable` prop: the sidebar makes the editor read-only for as long as it's open, and restores it on close. */} <BlockNoteView editor={editor} renderEditor={false}> <div className="layout"> <div className="editor-panel"> <BlockNoteViewEditor /> {!showSidebar && ( <button className="show-history-button" onClick={() => setShowSidebar(true)} > History </button> )} </div> {showSidebar && ( <div className={"sidebar-section"}> <VersioningSidebar onClose={() => setShowSidebar(false)} // Extend the row menu by composing it: the default items plus // an app-specific one. Order is yours to choose. snapshotMenu={ <VersionMenu> <DefaultVersionMenuItems /> <MakeCopyItem /> </VersionMenu> } /> </div> )} </div> </BlockNoteView> </div> );}/** * An application-specific row action. `useVersionSnapshot()` hands it the row * it was rendered in, so it needs no props — the sidebar knows nothing about it. */function MakeCopyItem() { const { snapshot, isCurrent } = useVersionSnapshot(); return ( <VersionMenuItem icon={<RiFileCopyLine />} onClick={() => { window.alert( `Would copy ${isCurrent ? "the current version" : (snapshot.name ?? new Date(snapshot.createdAt).toLocaleString())} into a new document.`, ); }} > Make a copy </VersionMenuItem> );}import type { PartialBlock } from "@blocknote/core";export const DAY_MS = 24 * 60 * 60 * 1000;export type SampleVersion = { name: string; /** How long ago the version was saved. */ daysAgo: number; blocks: PartialBlock[];};/** * A short launch-plan document at three points in its history, oldest first. * Block ids are stable across versions so that a diff between two of them * shows what actually changed rather than a wholesale replacement. */export const SAMPLE_HISTORY: SampleVersion[] = [ { name: "First draft", daysAgo: 9, blocks: [ { id: "title", type: "heading", props: { level: 2 }, content: "Launch plan: Notes 2.0", }, { id: "goal", type: "paragraph", content: "Goal: ship the new editor to every workspace before the end of the quarter.", }, { id: "milestones", type: "heading", props: { level: 3 }, content: "Milestones", }, { id: "m1", type: "bulletListItem", content: "Beta with five design partners", }, { id: "m3", type: "bulletListItem", content: "Public release" }, ], }, { name: "Added dates", daysAgo: 6, blocks: [ { id: "title", type: "heading", props: { level: 2 }, content: "Launch plan: Notes 2.0", }, { id: "goal", type: "paragraph", content: "Goal: ship the new editor to every workspace before the end of September.", }, { id: "milestones", type: "heading", props: { level: 3 }, content: "Milestones", }, { id: "m1", type: "bulletListItem", content: "Beta with five design partners (June)", }, { id: "m2", type: "bulletListItem", content: "Fix the ten most-reported beta issues", }, { id: "m3", type: "bulletListItem", content: "Public release (September)", }, ], }, { name: "Marketing review", daysAgo: 2, blocks: [ { id: "title", type: "heading", props: { level: 2 }, content: "Launch plan: Notes 2.0", }, { id: "goal", type: "paragraph", content: "Goal: ship the new editor to every workspace before the end of September.", }, { id: "milestones", type: "heading", props: { level: 3 }, content: "Milestones", }, { id: "m1", type: "bulletListItem", content: "Beta with five design partners (June)", }, { id: "m2", type: "bulletListItem", content: "Fix the ten most-reported beta issues", }, { id: "m3", type: "bulletListItem", content: "Public release (September 15)", }, { id: "announcement", type: "heading", props: { level: 3 }, content: "Announcement", }, { id: "announcement-text", type: "paragraph", content: "The blog post and changelog entry go out on release day. The newsletter follows a week later.", }, ], },];/** * The document as it is now: the newest version plus edits nobody has saved * yet, so the current version has something to compare against. */export const LIVE_DOCUMENT: PartialBlock[] = [ { id: "title", type: "heading", props: { level: 2 }, content: "Launch plan: Notes 2.0", }, { id: "goal", type: "paragraph", content: "Goal: ship the new editor to every workspace before the end of September, keeping the old editor available as a fallback for one release.", }, { id: "milestones", type: "heading", props: { level: 3 }, content: "Milestones", }, { id: "m1", type: "bulletListItem", content: "Beta with five design partners (June)", }, { id: "m2", type: "bulletListItem", content: "Fix the ten most-reported beta issues", }, { id: "m3", type: "bulletListItem", content: "Public release (September 15)", }, { id: "announcement", type: "heading", props: { level: 3 }, content: "Announcement", }, { id: "announcement-text", type: "paragraph", content: "The blog post and changelog entry go out on release day. The newsletter follows a week later.", }, { id: "questions", type: "heading", props: { level: 3 }, content: "Open questions", }, { id: "q1", type: "numberedListItem", content: "Do we keep the old editor available as a fallback?", }, { id: "q2", type: "numberedListItem", content: "Who owns the migration guide?", },];/* App layout only. The versioning sidebar's own styling (header, snapshot rows, selected/comparing states, the "..." menu) ships with the UI library (@blocknote/mantine etc.), so it isn't repeated here. */.wrapper { height: calc(100vh - 20px);}.wrapper > .bn-container { margin: 0; max-width: none; padding: 0;}.layout { display: flex; gap: 0; height: calc(100vh - 20px);}.editor-panel { flex: 1; height: calc(100vh - 20px); min-width: 0; overflow: auto; position: relative;}.editor-panel .bn-container { height: calc(100vh - 20px); margin: 0; max-width: none; padding: 0;}.editor-panel .bn-editor { height: calc(100vh - 20px); overflow: auto;}/* The history panel sits flush against the editor with a subtle divider. */.sidebar-section { background-color: var(--bn-colors-editor-background); border-left: 1px solid var(--bn-colors-border); box-shadow: -6px 0 16px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; height: calc(100vh - 20px); overflow: auto; width: 350px;}.dark .sidebar-section { border-left-color: #2c2c2c; box-shadow: -6px 0 16px rgba(0, 0, 0, 0.3);}.show-history-button { background-color: var(--bn-colors-menu-background); border: var(--bn-border); border-radius: var(--bn-border-radius-medium); box-shadow: var(--bn-shadow-medium); color: var(--bn-colors-menu-text); cursor: pointer; font-size: 13px; font-weight: 600; padding: 6px 12px; position: absolute; right: 16px; top: 16px;}