Skip to content

Commit 951fa7b

Browse files
committed
docs(examples): add container-block example
A Notion-style Callout block that holds nested blocks via the new `container` config, with a slash-menu insert and a live JSON panel.
1 parent 6ee83f9 commit 951fa7b

13 files changed

Lines changed: 506 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"playground": true,
3+
"docs": true,
4+
"author": "nickthesick",
5+
"tags": [
6+
"Intermediate",
7+
"Blocks",
8+
"Custom Schemas",
9+
"Suggestion Menus",
10+
"Slash Menu"
11+
],
12+
"dependencies": {
13+
"react-icons": "^5.5.0"
14+
}
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Container Block
2+
3+
In this example, we create a custom `Callout` block that holds **other blocks** as its body — like a Notion-style callout that can wrap a paragraph followed by a code block, or any combination of nested blocks.
4+
5+
The block uses the new `container` config on `BlockConfig`. Setting `container: { defaultBlocks: ["paragraph"] }` (with `content: "none"`) tells BlockNote to emit a ProseMirror node that holds nested `blockContainer+` children — the same shape that columns use under the hood. The contained blocks live on `block.children` at runtime.
6+
7+
We also wire up a Slash Menu item to insert the callout, and render the document JSON next to the editor so you can inspect the structure of the nested blocks.
8+
9+
**Try it out:**
10+
11+
- Press the "/" key inside the callout's body and add a code block, heading, or list — anything goes.
12+
- Watch the JSON panel on the right update as you edit; the callout's children appear in `block.children`.
13+
- Insert a new callout via the Slash Menu (search "callout").
14+
15+
**Relevant Docs:**
16+
17+
- [Custom Blocks](/docs/features/custom-schemas/custom-blocks)
18+
- [Editor Setup](/docs/getting-started/editor-setup)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="UTF-8" />
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
5+
<title>Container Block</title>
6+
<script>
7+
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
8+
</script>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="./main.tsx"></script>
13+
</body>
14+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
2+
import React from "react";
3+
import { createRoot } from "react-dom/client";
4+
import App from "./src/App.jsx";
5+
6+
const root = createRoot(document.getElementById("root")!);
7+
root.render(
8+
<React.StrictMode>
9+
<App />
10+
</React.StrictMode>,
11+
);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@blocknote/example-custom-schema-container-block",
3+
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
4+
"type": "module",
5+
"private": true,
6+
"version": "0.12.4",
7+
"scripts": {
8+
"start": "vp dev",
9+
"dev": "vp dev",
10+
"build:prod": "tsc && vp build",
11+
"preview": "vp preview"
12+
},
13+
"dependencies": {
14+
"@blocknote/ariakit": "latest",
15+
"@blocknote/core": "latest",
16+
"@blocknote/mantine": "latest",
17+
"@blocknote/react": "latest",
18+
"@blocknote/shadcn": "latest",
19+
"@mantine/core": "^9.0.2",
20+
"@mantine/hooks": "^9.0.2",
21+
"react": "^19.2.3",
22+
"react-dom": "^19.2.3",
23+
"react-icons": "^5.5.0"
24+
},
25+
"devDependencies": {
26+
"@types/react": "^19.2.3",
27+
"@types/react-dom": "^19.2.3",
28+
"@vitejs/plugin-react": "^6.0.1",
29+
"vite-plus": "^0.1.24"
30+
}
31+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";
2+
import {
3+
filterSuggestionItems,
4+
insertOrUpdateBlockForSlashMenu,
5+
} from "@blocknote/core/extensions";
6+
import "@blocknote/core/fonts/inter.css";
7+
import { BlockNoteView } from "@blocknote/mantine";
8+
import "@blocknote/mantine/style.css";
9+
import {
10+
SuggestionMenuController,
11+
getDefaultReactSlashMenuItems,
12+
useCreateBlockNote,
13+
} from "@blocknote/react";
14+
import { useEffect, useState } from "react";
15+
import { RiChatQuoteLine } from "react-icons/ri";
16+
17+
import { createCallout } from "./Callout";
18+
import "./styles.css";
19+
20+
// Schema with the default blocks plus our custom Callout container block.
21+
const schema = BlockNoteSchema.create().extend({
22+
blockSpecs: {
23+
...defaultBlockSpecs,
24+
callout: createCallout(),
25+
},
26+
});
27+
28+
// Slash menu item to insert a Callout. Because Callout is a container block,
29+
// inserting one with no children causes BlockNote to seed it with the block's
30+
// configured `defaultBlocks` (a single paragraph here).
31+
const insertCallout = (editor: typeof schema.BlockNoteEditor) => ({
32+
title: "Callout",
33+
subtext: "Container block that wraps other blocks",
34+
onItemClick: () =>
35+
insertOrUpdateBlockForSlashMenu(editor, {
36+
type: "callout",
37+
}),
38+
aliases: ["callout", "container", "alert", "note", "tip", "info"],
39+
group: "Basic blocks",
40+
icon: <RiChatQuoteLine />,
41+
});
42+
43+
type AppBlock = (typeof schema.BlockNoteEditor)["document"][number];
44+
45+
export default function App() {
46+
const [blocks, setBlocks] = useState<AppBlock[]>([]);
47+
48+
const editor = useCreateBlockNote({
49+
schema,
50+
initialContent: [
51+
{
52+
type: "paragraph",
53+
content: "Welcome — this demo shows the new `container` block kind.",
54+
},
55+
{
56+
type: "callout",
57+
props: { flavor: "tip" },
58+
children: [
59+
{
60+
type: "paragraph",
61+
content: "Callouts can hold any block as their body.",
62+
},
63+
{
64+
type: "paragraph",
65+
content:
66+
"Try pressing '/' inside this callout to add a heading or code block.",
67+
},
68+
],
69+
},
70+
{
71+
type: "paragraph",
72+
content: "Press '/' anywhere to insert a new Callout.",
73+
},
74+
{
75+
type: "paragraph",
76+
},
77+
],
78+
});
79+
80+
useEffect(() => setBlocks(editor.document), [editor]);
81+
82+
return (
83+
<div className={"wrapper"}>
84+
<div>BlockNote Editor:</div>
85+
<div className={"item"}>
86+
<BlockNoteView
87+
editor={editor}
88+
slashMenu={false}
89+
onChange={() => {
90+
setBlocks(editor.document);
91+
}}
92+
>
93+
<SuggestionMenuController
94+
triggerCharacter={"/"}
95+
getItems={async (query) => {
96+
const defaultItems = getDefaultReactSlashMenuItems(editor);
97+
const lastBasicBlockIndex = defaultItems.findLastIndex(
98+
(item) => item.group === "Basic blocks",
99+
);
100+
defaultItems.splice(
101+
lastBasicBlockIndex + 1,
102+
0,
103+
insertCallout(editor),
104+
);
105+
return filterSuggestionItems(defaultItems, query);
106+
}}
107+
/>
108+
</BlockNoteView>
109+
</div>
110+
<div>Document JSON:</div>
111+
<div className={"item bordered"}>
112+
<pre>
113+
<code>{JSON.stringify(blocks, null, 2)}</code>
114+
</pre>
115+
</div>
116+
</div>
117+
);
118+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { createReactBlockSpec, NodeViewWrapper } from "@blocknote/react";
2+
import { MdCheckCircle, MdInfo, MdLightbulb, MdWarning } from "react-icons/md";
3+
4+
import "./styles.css";
5+
6+
// The flavors of callout the user can switch between.
7+
export const calloutTypes = [
8+
{ value: "tip", title: "Tip", icon: MdLightbulb },
9+
{ value: "info", title: "Info", icon: MdInfo },
10+
{ value: "warning", title: "Warning", icon: MdWarning },
11+
{ value: "success", title: "Success", icon: MdCheckCircle },
12+
] as const;
13+
14+
// The Callout block. Declared with `content: "none"` plus the new `container`
15+
// config — the block hosts arbitrary child blocks in its body, exposed at
16+
// runtime as `block.children`.
17+
export const createCallout = createReactBlockSpec(
18+
{
19+
type: "callout",
20+
propSchema: {
21+
flavor: {
22+
default: "tip",
23+
values: ["tip", "info", "warning", "success"],
24+
},
25+
},
26+
content: "none",
27+
container: {
28+
min: 1,
29+
defaultBlocks: ["paragraph"],
30+
},
31+
},
32+
{
33+
render: (props) => {
34+
const flavor =
35+
calloutTypes.find((c) => c.value === props.block.props.flavor) ??
36+
calloutTypes[0];
37+
const Icon = flavor.icon;
38+
39+
const cycleFlavor = () => {
40+
const idx = calloutTypes.findIndex(
41+
(c) => c.value === props.block.props.flavor,
42+
);
43+
const next = calloutTypes[(idx + 1) % calloutTypes.length];
44+
props.editor.updateBlock(props.block, {
45+
type: "callout",
46+
props: { flavor: next.value },
47+
});
48+
};
49+
50+
return (
51+
<NodeViewWrapper
52+
className={"callout"}
53+
data-node-type="callout"
54+
data-id={props.block.id}
55+
data-flavor={flavor.value}
56+
>
57+
<button
58+
className={"callout-icon-button"}
59+
type={"button"}
60+
contentEditable={false}
61+
onClick={cycleFlavor}
62+
title={`Click to cycle flavor (current: ${flavor.title})`}
63+
>
64+
<Icon size={20} />
65+
</button>
66+
<div className={"callout-body"} ref={props.contentRef} />
67+
</NodeViewWrapper>
68+
);
69+
},
70+
},
71+
);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
.wrapper {
2+
display: flex;
3+
flex-direction: column;
4+
height: 100%;
5+
}
6+
7+
.item {
8+
border-radius: 0.5rem;
9+
flex: 1;
10+
overflow: hidden;
11+
}
12+
13+
.item.bordered {
14+
border: 1px solid gray;
15+
}
16+
17+
.item pre {
18+
border-radius: 0.5rem;
19+
height: 100%;
20+
overflow: auto;
21+
padding-block: 1rem;
22+
padding-inline: 54px;
23+
width: 100%;
24+
white-space: pre-wrap;
25+
}
26+
27+
.callout {
28+
display: flex;
29+
align-items: flex-start;
30+
gap: 12px;
31+
flex-grow: 1;
32+
border-radius: 6px;
33+
padding: 12px 16px;
34+
border-left: 4px solid var(--callout-accent, #888);
35+
background-color: var(--callout-bg, #f3f4f6);
36+
}
37+
38+
.callout[data-flavor="tip"] {
39+
--callout-accent: #d97706;
40+
--callout-bg: #fff7ed;
41+
}
42+
43+
.callout[data-flavor="info"] {
44+
--callout-accent: #507aff;
45+
--callout-bg: #e6ebff;
46+
}
47+
48+
.callout[data-flavor="warning"] {
49+
--callout-accent: #b91c1c;
50+
--callout-bg: #fef2f2;
51+
}
52+
53+
.callout[data-flavor="success"] {
54+
--callout-accent: #16a34a;
55+
--callout-bg: #ecfdf5;
56+
}
57+
58+
[data-color-scheme="dark"] .callout[data-flavor="tip"] {
59+
--callout-bg: #432e0e;
60+
}
61+
62+
[data-color-scheme="dark"] .callout[data-flavor="info"] {
63+
--callout-bg: #1e2a5c;
64+
}
65+
66+
[data-color-scheme="dark"] .callout[data-flavor="warning"] {
67+
--callout-bg: #4a1212;
68+
}
69+
70+
[data-color-scheme="dark"] .callout[data-flavor="success"] {
71+
--callout-bg: #0d3b21;
72+
}
73+
74+
.callout-icon-button {
75+
background: none;
76+
border: none;
77+
cursor: pointer;
78+
padding: 4px;
79+
color: var(--callout-accent, #888);
80+
display: flex;
81+
align-items: center;
82+
justify-content: center;
83+
margin-top: 2px;
84+
}
85+
86+
.callout-icon-button:hover {
87+
opacity: 0.75;
88+
}
89+
90+
.callout-body {
91+
flex-grow: 1;
92+
min-width: 0;
93+
}

0 commit comments

Comments
 (0)