diff --git a/examples/01-basic/17-no-trailing-block/.bnexample.json b/examples/01-basic/17-no-trailing-block/.bnexample.json
new file mode 100644
index 0000000000..e9c8bcb27b
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/.bnexample.json
@@ -0,0 +1,6 @@
+{
+ "playground": true,
+ "docs": false,
+ "author": "matthewlipski",
+ "tags": ["Basic"]
+}
diff --git a/examples/01-basic/17-no-trailing-block/README.md b/examples/01-basic/17-no-trailing-block/README.md
new file mode 100644
index 0000000000..9c63e46fdd
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/README.md
@@ -0,0 +1,7 @@
+# No Trailing Block
+
+This example shows how to disable the automatic creation of a trailing block at the end of the editor by setting the `trailingBlock` option to `false`.
+
+**Relevant Docs:**
+
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/01-basic/17-no-trailing-block/index.html b/examples/01-basic/17-no-trailing-block/index.html
new file mode 100644
index 0000000000..a86933f050
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ No Trailing Block
+
+
+
+
+
+
+
diff --git a/examples/01-basic/17-no-trailing-block/main.tsx b/examples/01-basic/17-no-trailing-block/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/main.tsx
@@ -0,0 +1,11 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./src/App.jsx";
+
+const root = createRoot(document.getElementById("root")!);
+root.render(
+
+
+
+);
diff --git a/examples/01-basic/17-no-trailing-block/package.json b/examples/01-basic/17-no-trailing-block/package.json
new file mode 100644
index 0000000000..84ad1d6734
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-basic-no-trailing-block",
+ "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "type": "module",
+ "private": true,
+ "version": "0.12.4",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build:prod": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@blocknote/ariakit": "latest",
+ "@blocknote/core": "latest",
+ "@blocknote/mantine": "latest",
+ "@blocknote/react": "latest",
+ "@blocknote/shadcn": "latest",
+ "@mantine/core": "^8.3.11",
+ "@mantine/hooks": "^8.3.11",
+ "@mantine/utils": "^6.0.22",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.3",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "vite": "^8.0.8"
+ }
+}
\ No newline at end of file
diff --git a/examples/01-basic/17-no-trailing-block/src/App.tsx b/examples/01-basic/17-no-trailing-block/src/App.tsx
new file mode 100644
index 0000000000..ac51ec74b3
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/src/App.tsx
@@ -0,0 +1,14 @@
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import { useCreateBlockNote } from "@blocknote/react";
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ trailingBlock: false,
+ });
+
+ // Renders the editor instance using a React component.
+ return ;
+}
diff --git a/examples/01-basic/17-no-trailing-block/tsconfig.json b/examples/01-basic/17-no-trailing-block/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "composite": true
+ },
+ "include": [
+ "."
+ ],
+ "__ADD_FOR_LOCAL_DEV_references": [
+ {
+ "path": "../../../packages/core/"
+ },
+ {
+ "path": "../../../packages/react/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/01-basic/17-no-trailing-block/vite.config.ts b/examples/01-basic/17-no-trailing-block/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/01-basic/17-no-trailing-block/vite.config.ts
@@ -0,0 +1,32 @@
+// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
+import react from "@vitejs/plugin-react";
+import * as fs from "fs";
+import * as path from "path";
+import { defineConfig } from "vite";
+// import eslintPlugin from "vite-plugin-eslint";
+// https://vitejs.dev/config/
+export default defineConfig((conf) => ({
+ plugins: [react()],
+ optimizeDeps: {},
+ build: {
+ sourcemap: true,
+ },
+ resolve: {
+ alias:
+ conf.command === "build" ||
+ !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ ? {}
+ : ({
+ // Comment out the lines below to load a built version of blocknote
+ // or, keep as is to load live from sources with live reload working
+ "@blocknote/core": path.resolve(
+ __dirname,
+ "../../packages/core/src/"
+ ),
+ "@blocknote/react": path.resolve(
+ __dirname,
+ "../../packages/react/src/"
+ ),
+ } as any),
+ },
+}));
diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
index bd4a517900..015cf22420 100644
--- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
+++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts
@@ -9,6 +9,7 @@ import {
} from "../../schema/index.js";
import { formatKeyboardShortcut } from "../../util/browser.js";
import { FilePanelExtension } from "../FilePanel/FilePanel.js";
+import { FormattingToolbarExtension } from "../FormattingToolbar/FormattingToolbar.js";
import { DefaultSuggestionItem } from "./DefaultSuggestionItem.js";
import { SuggestionMenu } from "./SuggestionMenu.js";
@@ -242,6 +243,11 @@ export function getDefaultSlashMenuItems<
// Immediately open the file toolbar
editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id);
+ // Immediately hide the formatting toolbar. This is only necessary for
+ // when the `trailingBlock` editor option is set to `false` and the
+ // inserted block is at the end of the document. Otherwise, the
+ // selection moves to the next block with inline content.
+ editor.getExtension(FormattingToolbarExtension)?.store.setState(false);
},
key: "image",
...editor.dictionary.slash_menu.image,
@@ -257,6 +263,11 @@ export function getDefaultSlashMenuItems<
// Immediately open the file toolbar
editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id);
+ // Immediately hide the formatting toolbar. This is only necessary for
+ // when the `trailingBlock` editor option is set to `false` and the
+ // inserted block is at the end of the document. Otherwise, the
+ // selection moves to the next block with inline content.
+ editor.getExtension(FormattingToolbarExtension)?.store.setState(false);
},
key: "video",
...editor.dictionary.slash_menu.video,
@@ -272,6 +283,11 @@ export function getDefaultSlashMenuItems<
// Immediately open the file toolbar
editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id);
+ // Immediately hide the formatting toolbar. This is only necessary for
+ // when the `trailingBlock` editor option is set to `false` and the
+ // inserted block is at the end of the document. Otherwise, the
+ // selection moves to the next block with inline content.
+ editor.getExtension(FormattingToolbarExtension)?.store.setState(false);
},
key: "audio",
...editor.dictionary.slash_menu.audio,
@@ -287,6 +303,11 @@ export function getDefaultSlashMenuItems<
// Immediately open the file toolbar
editor.getExtension(FilePanelExtension)?.showMenu(insertedBlock.id);
+ // Immediately hide the formatting toolbar. This is only necessary for
+ // when the `trailingBlock` editor option is set to `false` and the
+ // inserted block is at the end of the document. Otherwise, the
+ // selection moves to the next block with inline content.
+ editor.getExtension(FormattingToolbarExtension)?.store.setState(false);
},
key: "file",
...editor.dictionary.slash_menu.file,
diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index ae117ce392..868ff5b7aa 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -323,6 +323,25 @@
},
"readme": "This example makes the editor read-only while showing the same content as the [Default Schema Showcase](/examples/basic/default-blocks) example.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)\n- [Document Structure](/docs/foundations/document-structure)\n- [Default Schema](/docs/foundations/schemas)"
},
+ {
+ "projectSlug": "no-trailing-block",
+ "fullSlug": "basic/no-trailing-block",
+ "pathFromRoot": "examples/01-basic/17-no-trailing-block",
+ "config": {
+ "playground": true,
+ "docs": false,
+ "author": "matthewlipski",
+ "tags": [
+ "Basic"
+ ]
+ },
+ "title": "No Trailing Block",
+ "group": {
+ "pathFromRoot": "examples/01-basic",
+ "slug": "basic"
+ },
+ "readme": "This example shows how to disable the automatic creation of a trailing block at the end of the editor by setting the `trailingBlock` option to `false`.\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)"
+ },
{
"projectSlug": "testing",
"fullSlug": "basic/testing",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 966cf9e887..8ec1de17a4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1110,6 +1110,52 @@ importers:
specifier: ^8.0.8
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3)
+ examples/01-basic/17-no-trailing-block:
+ dependencies:
+ '@blocknote/ariakit':
+ specifier: latest
+ version: link:../../../packages/ariakit
+ '@blocknote/core':
+ specifier: latest
+ version: link:../../../packages/core
+ '@blocknote/mantine':
+ specifier: latest
+ version: link:../../../packages/mantine
+ '@blocknote/react':
+ specifier: latest
+ version: link:../../../packages/react
+ '@blocknote/shadcn':
+ specifier: latest
+ version: link:../../../packages/shadcn
+ '@mantine/core':
+ specifier: ^8.3.11
+ version: 8.3.18(@mantine/hooks@8.3.18(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ '@mantine/hooks':
+ specifier: ^8.3.11
+ version: 8.3.18(react@19.2.5)
+ '@mantine/utils':
+ specifier: ^6.0.22
+ version: 6.0.22(react@19.2.5)
+ react:
+ specifier: ^19.2.3
+ version: 19.2.5
+ react-dom:
+ specifier: ^19.2.3
+ version: 19.2.5(react@19.2.5)
+ devDependencies:
+ '@types/react':
+ specifier: ^19.2.3
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.14)
+ '@vitejs/plugin-react':
+ specifier: ^6.0.1
+ version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3))
+ vite:
+ specifier: ^8.0.8
+ version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.3)
+
examples/01-basic/testing:
dependencies:
'@blocknote/ariakit':
@@ -24078,8 +24124,8 @@ snapshots:
'@next/eslint-plugin-next': 16.2.2
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1))
@@ -24128,7 +24174,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -24139,7 +24185,7 @@ snapshots:
tinyglobby: 0.2.16
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -24153,14 +24199,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -24201,7 +24247,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -24212,7 +24258,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.10
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
diff --git a/tests/src/end-to-end/images/images.test.ts b/tests/src/end-to-end/images/images.test.ts
index b4c7dd2910..83f9045152 100644
--- a/tests/src/end-to-end/images/images.test.ts
+++ b/tests/src/end-to-end/images/images.test.ts
@@ -1,18 +1,15 @@
import { FileChooser, expect } from "@playwright/test";
import { test } from "../../setup/setupScript.js";
-import { BASE_URL } from "../../utils/const.js";
+import { BASE_URL, NO_TRAILING_BLOCK_URL } from "../../utils/const.js";
import { compareDocToSnapshot, focusOnEditor } from "../../utils/editor.js";
import { executeSlashCommand } from "../../utils/slashmenu.js";
const IMAGE_UPLOAD_PATH = "src/end-to-end/images/placeholder.png";
const IMAGE_EMBED_URL = "https://placehold.co/800x540.png";
-test.beforeEach(async ({ page }) => {
- await page.goto(BASE_URL);
-});
-
test.describe("Check Image Block and Toolbar functionality", () => {
test("Should be able to create image block", async ({ page }) => {
+ await page.goto(BASE_URL);
await focusOnEditor(page);
await executeSlashCommand(page, "image");
@@ -21,6 +18,7 @@ test.describe("Check Image Block and Toolbar functionality", () => {
expect(await page.screenshot()).toMatchSnapshot("create-image.png");
});
test.skip("Should be able to upload image", async ({ page }) => {
+ await page.goto(BASE_URL);
await focusOnEditor(page);
await executeSlashCommand(page, "image");
@@ -37,6 +35,7 @@ test.describe("Check Image Block and Toolbar functionality", () => {
expect(await page.screenshot()).toMatchSnapshot("upload-image.png");
});
test("Should be able to embed image", async ({ page }) => {
+ await page.goto(BASE_URL);
await focusOnEditor(page);
await executeSlashCommand(page, "image");
@@ -54,6 +53,7 @@ test.describe("Check Image Block and Toolbar functionality", () => {
expect(await page.screenshot()).toMatchSnapshot("embed-image.png");
});
test("Should be able to resize image", async ({ page }) => {
+ await page.goto(BASE_URL);
await focusOnEditor(page);
await executeSlashCommand(page, "image");
@@ -95,6 +95,7 @@ test.describe("Check Image Block and Toolbar functionality", () => {
expect(await page.screenshot()).toMatchSnapshot("resize-image.png");
});
test("Should be able to delete image with backspace", async ({ page }) => {
+ await page.goto(BASE_URL);
await focusOnEditor(page);
await executeSlashCommand(page, "image");
@@ -109,4 +110,17 @@ test.describe("Check Image Block and Toolbar functionality", () => {
await compareDocToSnapshot(page, "deleteImage");
});
+ test("Should open file panel but not formatting toolbar when inserting image with no trailing block", async ({
+ page,
+ }) => {
+ await page.goto(NO_TRAILING_BLOCK_URL);
+ await focusOnEditor(page);
+ await executeSlashCommand(page, "image");
+
+ const filePanel = page.locator(".bn-panel");
+ await expect(filePanel).toBeVisible();
+
+ const formattingToolbar = page.locator(".bn-formatting-toolbar");
+ await expect(formattingToolbar).not.toBeVisible();
+ });
});
diff --git a/tests/src/utils/const.ts b/tests/src/utils/const.ts
index 7431d2db2a..f7ef0eac35 100644
--- a/tests/src/utils/const.ts
+++ b/tests/src/utils/const.ts
@@ -51,6 +51,10 @@ export const COMMENTS_URL = !process.env.RUN_IN_DOCKER
? `http://localhost:${PORT}/collaboration/comments-testing?hideMenu`
: `http://host.docker.internal:${PORT}/collaboration/comments-testing?hideMenu`;
+export const NO_TRAILING_BLOCK_URL = !process.env.RUN_IN_DOCKER
+ ? `http://localhost:${PORT}/basic/no-trailing-block?hideMenu`
+ : `http://host.docker.internal:${PORT}/basic/no-trailing-block?hideMenu`;
+
export const PASTE_ZONE_SELECTOR = "#pasteZone";
export const EDITOR_SELECTOR = `.bn-editor`;