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.
Try it out: Edit the document, then click the "Version History" button to open the sidebar. From there you can save snapshots, preview older versions, rename them, and restore them.
import "@blocknote/core/fonts/inter.css";import { VersioningExtension, createInMemoryVersioningAdapter,} from "@blocknote/core/extensions";import { BlockNoteViewEditor, useCreateBlockNote, useExtension, useExtensionState,} from "@blocknote/react";import { BlockNoteView } from "@blocknote/mantine";import "@blocknote/mantine/style.css";import { useState } from "react";import { RiHistoryLine } from "react-icons/ri";import { VersionHistorySidebar } from "./VersionHistorySidebar";import "./style.css";export default function App() { // `createInMemoryVersioningAdapter` is passed as a factory function. The // VersioningExtension will call it with the editor instance once it's ready. const editor = useCreateBlockNote({ initialContent: [ { type: "heading", content: "In-Memory Versioning Example", props: { level: 2 }, }, { type: "paragraph", content: "This example demonstrates versioning without any collaboration layer. " + "Snapshots are stored in memory using ProseMirror JSON — no Yjs required.", }, { type: "paragraph", content: "Try editing this document, then open the Version History sidebar to " + "save snapshots. You can preview and restore older versions.", }, ], extensions: [VersioningExtension(createInMemoryVersioningAdapter)], }); const { exitPreview } = useExtension(VersioningExtension, { editor }); const { previewedSnapshotId } = useExtensionState(VersioningExtension, { editor, }); const [sidebar, setSidebar] = useState<"versionHistory" | "none">("none"); return ( <div className="versioning-example"> <BlockNoteView editor={editor} editable={ sidebar !== "versionHistory" || previewedSnapshotId === undefined } renderEditor={false} > <div className="main-container"> <div className={"editor-layout-wrapper"}> <div className="sidebar-selectors"> <div className={`sidebar-selector ${sidebar === "versionHistory" ? "selected" : ""}`} onClick={() => { setSidebar((s) => s !== "versionHistory" ? "versionHistory" : "none", ); exitPreview(); }} > <RiHistoryLine /> <span>Version History</span> </div> </div> <div className={"editor-section"}> <BlockNoteViewEditor /> </div> </div> {sidebar === "versionHistory" && <VersionHistorySidebar />} </div> </BlockNoteView> </div> );}import { ComponentProps, useComponentsContext } from "@blocknote/react";// This component is used to display a selection dropdown with a label. By using// the useComponentsContext hook, we can create it out of existing components// within the same UI library that `BlockNoteView` uses (Mantine, Ariakit, or// ShadCN), to match the design of the editor.export const SettingsSelect = (props: { label: string; items: ComponentProps["FormattingToolbar"]["Select"]["items"];}) => { const Components = useComponentsContext()!; return ( <div className={"settings-select"}> <Components.Generic.Toolbar.Root className={"bn-toolbar"}> <h2>{props.label + ":"}</h2> <Components.Generic.Toolbar.Select className={"bn-select"} items={props.items} /> </Components.Generic.Toolbar.Root> </div> );};import { VersioningSidebar } from "@blocknote/react";import { useState } from "react";import { SettingsSelect } from "./SettingsSelect";export const VersionHistorySidebar = () => { const [filter, setFilter] = useState<"named" | "all">("all"); return ( <div className={"sidebar-section"}> <div className={"settings"}> <SettingsSelect label={"Filter"} items={[ { text: "All", icon: null, onClick: () => setFilter("all"), isSelected: filter === "all", }, { text: "Named", icon: null, onClick: () => setFilter("named"), isSelected: filter === "named", }, ]} /> </div> <VersioningSidebar filter={filter} /> </div> );};.versioning-example { align-items: flex-end; background-color: var(--bn-colors-disabled-background); display: flex; flex-direction: column; gap: 10px; height: 100%; max-width: none; overflow: auto; padding: 10px;}.versioning-example .main-container { display: flex; gap: 10px; height: 100%; max-width: none; width: 100%;}.versioning-example .editor-layout-wrapper { align-items: center; display: flex; flex: 2; flex-direction: column; gap: 10px; justify-content: center; width: 100%;}.versioning-example .sidebar-selectors { align-items: center; display: flex; flex-direction: row; gap: 10px; justify-content: space-between; max-width: 700px; width: 100%;}.versioning-example .sidebar-selector { align-items: center; background-color: var(--bn-colors-menu-background); border-radius: var(--bn-border-radius-medium); box-shadow: var(--bn-shadow-medium); color: var(--bn-colors-menu-text); cursor: pointer; display: flex; flex-direction: row; font-family: var(--bn-font-family); font-weight: 600; gap: 8px; justify-content: center; padding: 10px; user-select: none; width: 100%;}.versioning-example .sidebar-selector:hover { background-color: var(--bn-colors-hovered-background); color: var(--bn-colors-hovered-text);}.versioning-example .sidebar-selector.selected { background-color: var(--bn-colors-selected-background); color: var(--bn-colors-selected-text);}.versioning-example .sidebar-section { border-radius: var(--bn-border-radius-large); box-shadow: var(--bn-shadow-medium); display: flex; flex-direction: column; max-height: 100%; min-width: 350px; width: 100%;}.versioning-example .bn-editor,.versioning-example .bn-versioning-sidebar { border-radius: var(--bn-border-radius-medium); display: flex; flex-direction: column; gap: 10px; height: 100%; overflow: auto;}.versioning-example .editor-section { background-color: var(--bn-colors-editor-background); border-radius: var(--bn-border-radius-large); display: block; height: 90vh; max-width: 700px;}.versioning-example .sidebar-section { background-color: var(--bn-colors-editor-background); border-radius: var(--bn-border-radius-large); width: 350px;}.versioning-example .sidebar-section .settings { padding-block: 16px; padding-inline: 16px;}.versioning-example .bn-versioning-sidebar { padding-inline: 16px;}.versioning-example .settings { display: flex; flex-wrap: wrap; gap: 10px;}.versioning-example .settings-select { display: flex; gap: 10px;}.versioning-example .settings-select .bn-toolbar { align-items: center;}.versioning-example .settings-select h2 { color: var(--bn-colors-menu-text); margin: 0; font-size: 12px; line-height: 12px; padding-left: 14px;}.versioning-example .bn-snapshot { 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; flex-direction: column; gap: 16px; display: flex; overflow: visible; padding: 16px 32px; width: 100%;}.versioning-example .bn-snapshot-name { background: transparent; border: none; color: var(--bn-colors-menu-text); font-size: 16px; font-weight: 600; padding: 0; width: 100%;}.versioning-example .bn-snapshot-name:focus { outline: none;}.versioning-example .bn-snapshot-body { display: flex; flex-direction: column; font-size: 12px; gap: 4px;}.versioning-example .bn-snapshot-button { background-color: #4da3ff; border: none; border-radius: 4px; color: var(--bn-colors-selected-text); cursor: pointer; font-size: 12px; font-weight: 600; padding: 0 8px; width: fit-content;}.dark .bn-snapshot-button { background-color: #0070e8;}.versioning-example .bn-snapshot-button:hover { background-color: #73b7ff;}.dark .bn-snapshot-button:hover { background-color: #3785d8;}.versioning-example .bn-versioning-sidebar .bn-snapshot.selected { background-color: #f5f9fd; border: 2px solid #c2dcf8;}.dark .bn-versioning-sidebar .bn-snapshot.selected { background-color: #20242a; border: 2px solid #23405b;}