diff --git a/docs/content/docs/react/components/suggestion-menus.mdx b/docs/content/docs/react/components/suggestion-menus.mdx
index db9b8421a6..c44f18af45 100644
--- a/docs/content/docs/react/components/suggestion-menus.mdx
+++ b/docs/content/docs/react/components/suggestion-menus.mdx
@@ -58,6 +58,70 @@ Passing `slashMenu={false}` to `BlockNoteView` tells BlockNote not to show the d
`getItems` should return the items that need to be shown in the Slash Menu, based on a `query` entered by the user (anything the user types after the `triggerCharacter`). In this case, we simply append the "Hello World" item to the default Slash Menu items, and use `filterSuggestionItems` to filter the full list of items based on the user query.
+### Item Grouping & Ordering
+
+Slash Menu items are rendered in the same order as the items returned from `getItems`. Adjacent items which share the same `group` attribute are rendered together in the same group under a single label.
+
+#### Ordering
+
+Items appear in the menu in the exact order of the array. Reordering the array reorders the menu:
+
+```typescript
+getItems={async (query) =>
+ filterSuggestionItems(
+ [
+ insertHelloWorldItem(editor), // Shown first
+ ...getDefaultReactSlashMenuItems(editor), // Shown after
+ ],
+ query,
+ )
+}
+```
+
+#### Grouping
+
+Items with the same `group` attribute must be **adjacent** in the array to be rendered as one group. If items with the same `group` are separated by items with a different `group`, they will be rendered as two separate groups, each with their own label:
+
+```typescript
+// Renders as a single "Basic" group:
+[
+ { title: "Item A", group: "Basic", /* ... */ },
+ { title: "Item B", group: "Basic", /* ... */ },
+ { title: "Item C", group: "Other", /* ... */ },
+]
+
+// Renders as two separate "Basic" groups, with "Other" between them:
+[
+ { title: "Item A", group: "Basic", /* ... */ },
+ { title: "Item C", group: "Other", /* ... */ },
+ { title: "Item B", group: "Basic", /* ... */ },
+]
+```
+
+#### Finding, Inserting, Removing & Reordering Items
+
+Use regular array operations to manipulate items. For example, to insert a custom item directly after the default `Heading 1` item:
+
+```typescript
+const items = getDefaultReactSlashMenuItems(editor);
+const headingIndex = items.findIndex((item) => item.title === "Heading 1");
+items.splice(headingIndex + 1, 0, insertHelloWorldItem(editor));
+```
+
+To remove an item:
+
+```typescript
+const items = getDefaultReactSlashMenuItems(editor).filter(
+ (item) => item.title !== "Heading 1",
+);
+```
+
+To reorder items, sort or rearrange the array however you'd like before returning it from `getItems`.
+
+The demo below combines these techniques to render only the "Basic blocks" and "Headings" groups, with their order swapped:
+
+
+
### Replacing the Slash Menu Component
You can replace the React component used for the Slash Menu with your own, as you can see in the demo below.
diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/.bnexample.json b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/.bnexample.json
new file mode 100644
index 0000000000..8aa4573b06
--- /dev/null
+++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/.bnexample.json
@@ -0,0 +1,12 @@
+{
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": [
+ "Intermediate",
+ "Blocks",
+ "UI Components",
+ "Suggestion Menus",
+ "Slash Menu"
+ ]
+}
diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/README.md b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/README.md
new file mode 100644
index 0000000000..76f3061571
--- /dev/null
+++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/README.md
@@ -0,0 +1,11 @@
+# Slash Menu Grouping & Ordering
+
+In this example, we filter and reorder the default Slash Menu items so that only the "Basic blocks" and "Headings" groups are shown, with "Basic blocks" appearing first.
+
+**Try it out:** Press the "/" key to open the Slash Menu and see the reordered groups!
+
+**Relevant Docs:**
+
+- [Item Grouping & Ordering](/docs/react/components/suggestion-menus)
+- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)
+- [Editor Setup](/docs/getting-started/editor-setup)
diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/index.html b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/index.html
new file mode 100644
index 0000000000..405a9fc360
--- /dev/null
+++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Slash Menu Grouping & Ordering
+
+
+
+
+
+
+
diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/main.tsx b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/main.tsx
new file mode 100644
index 0000000000..677c7f7eed
--- /dev/null
+++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/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/03-ui-components/19-suggestion-menus-grouping-ordering/package.json b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/package.json
new file mode 100644
index 0000000000..1b60d694c5
--- /dev/null
+++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@blocknote/example-ui-components-suggestion-menus-grouping-ordering",
+ "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/03-ui-components/19-suggestion-menus-grouping-ordering/src/App.tsx b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/src/App.tsx
new file mode 100644
index 0000000000..80d5e59225
--- /dev/null
+++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/src/App.tsx
@@ -0,0 +1,63 @@
+import { BlockNoteEditor } from "@blocknote/core";
+import { filterSuggestionItems } from "@blocknote/core/extensions";
+import "@blocknote/core/fonts/inter.css";
+import { BlockNoteView } from "@blocknote/mantine";
+import "@blocknote/mantine/style.css";
+import {
+ DefaultReactSuggestionItem,
+ getDefaultReactSlashMenuItems,
+ SuggestionMenuController,
+ useCreateBlockNote,
+} from "@blocknote/react";
+
+// Returns the default Slash Menu items, keeping only the "Basic blocks" and
+// "Headings" groups, with "Basic blocks" listed before "Headings".
+const getCustomSlashMenuItems = (
+ editor: BlockNoteEditor,
+): DefaultReactSuggestionItem[] => {
+ const defaultItems = getDefaultReactSlashMenuItems(editor);
+
+ const basicBlocks = defaultItems.filter(
+ (item) => item.group === "Basic blocks",
+ );
+ const headings = defaultItems.filter((item) => item.group === "Headings");
+
+ return [...basicBlocks, ...headings];
+};
+
+export default function App() {
+ // Creates a new editor instance.
+ const editor = useCreateBlockNote({
+ initialContent: [
+ {
+ type: "paragraph",
+ content: "Welcome to this demo!",
+ },
+ {
+ type: "paragraph",
+ content: "Press the '/' key to open the Slash Menu",
+ },
+ {
+ type: "paragraph",
+ content:
+ "Notice that only 'Basic blocks' and 'Headings' are shown, in that order",
+ },
+ {
+ type: "paragraph",
+ },
+ ],
+ });
+
+ // Renders the editor instance.
+ return (
+
+
+ filterSuggestionItems(getCustomSlashMenuItems(editor), query)
+ }
+ />
+
+ );
+}
diff --git a/examples/03-ui-components/19-suggestion-menus-grouping-ordering/tsconfig.json b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/tsconfig.json
new file mode 100644
index 0000000000..dbe3e6f62d
--- /dev/null
+++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/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/03-ui-components/19-suggestion-menus-grouping-ordering/vite.config.ts b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/vite.config.ts
new file mode 100644
index 0000000000..f62ab20bc2
--- /dev/null
+++ b/examples/03-ui-components/19-suggestion-menus-grouping-ordering/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/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx
index ae117ce392..ccfc674ac5 100644
--- a/playground/src/examples.gen.tsx
+++ b/playground/src/examples.gen.tsx
@@ -851,6 +851,29 @@
"slug": "ui-components"
},
"readme": "This example demonstrates how to use the `DRAG_EXCLUSION_CLASSNAME` to create separate drag & drop areas that don't interfere with BlockNote's built-in block drag & drop functionality.\n\n## Features\n\n- **Drag Exclusion**: Elements with the `bn-drag-exclude` classname are treated as separate drag & drop operations\n- **Independent Drag Areas**: Create custom drag & drop functionality alongside BlockNote's editor\n- **No Interference**: Custom drag operations won't trigger BlockNote's block reordering\n- **Side-by-side Demo**: Shows the editor and custom drag area working independently\n\n## How It Works\n\nBy adding the `DRAG_EXCLUSION_CLASSNAME` (`bn-drag-exclude`) to an element, you tell BlockNote's drag & drop handlers to ignore all drag events within that element and its children. This allows you to implement your own custom drag & drop logic without conflicts.\n\nThe exclusion check works by traversing up the DOM tree from the drag event target, checking if any ancestor has the exclusion classname. If found, BlockNote's handlers return early, leaving your custom handlers in full control.\n\n## Code Highlights\n\n### Import the constant:\n\n```tsx\nimport { DRAG_EXCLUSION_CLASSNAME } from \"@blocknote/core\";\n```\n\n### Apply it to your custom drag area:\n\n```tsx\n\n {/* Your custom drag & drop UI */}\n
\n Custom draggable items\n
\n
\n```\n\n## Use Cases\n\n- **Custom UI elements**: Add draggable components within or near the editor\n- **File upload areas**: Create drag-and-drop file upload zones\n- **Sortable lists**: Implement custom sortable lists alongside the editor\n- **External integrations**: Integrate with third-party drag & drop libraries\n\n**Relevant Docs:**\n\n- [Side Menu (Drag Handle)](/docs/react/components/side-menu)\n- [Editor Setup](/docs/getting-started/editor-setup)"
+ },
+ {
+ "projectSlug": "suggestion-menus-grouping-ordering",
+ "fullSlug": "ui-components/suggestion-menus-grouping-ordering",
+ "pathFromRoot": "examples/03-ui-components/19-suggestion-menus-grouping-ordering",
+ "config": {
+ "playground": true,
+ "docs": true,
+ "author": "matthewlipski",
+ "tags": [
+ "Intermediate",
+ "Blocks",
+ "UI Components",
+ "Suggestion Menus",
+ "Slash Menu"
+ ]
+ },
+ "title": "Slash Menu Grouping & Ordering",
+ "group": {
+ "pathFromRoot": "examples/03-ui-components",
+ "slug": "ui-components"
+ },
+ "readme": "In this example, we filter and reorder the default Slash Menu items so that only the \"Basic blocks\" and \"Headings\" groups are shown, with \"Basic blocks\" appearing first.\n\n**Try it out:** Press the \"/\" key to open the Slash Menu and see the reordered groups!\n\n**Relevant Docs:**\n\n- [Item Grouping & Ordering](/docs/react/components/suggestion-menus)\n- [Changing Slash Menu Items](/docs/react/components/suggestion-menus)\n- [Editor Setup](/docs/getting-started/editor-setup)"
}
]
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 966cf9e887..5053fe78f3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2231,6 +2231,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/03-ui-components/19-suggestion-menus-grouping-ordering:
+ 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/04-theming/01-theming-dom-attributes:
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