ExamplesCustom SchemasContainer Block

Container Block

In this example, we create a custom Panel block that holds other blocks as its body, like a Notion-style callout wrapping a paragraph followed by a code block.

The block declares the children config on BlockConfig. children: { allow: "blocks" } makes it a container: its child blocks mount into the frame's slot (attached with ref={contentRef}), and live on block.children at runtime. A pure container like this draws its box in renderFrame alone, which re-renders live when props change — click the icon to cycle the panel's flavor and watch the box follow without rebuilding the body.

We also wire up a Slash Menu item to insert the panel, and render the document JSON next to the editor so you can inspect the structure of the nested blocks.

Try it out:

  • Press the "/" key inside the panel's body and add a code block, heading, or list.
  • Click the panel's icon to cycle its flavor. The box re-renders in place; the children are untouched.
  • Watch the JSON panel on the right update as you edit; the panel's children appear in block.children.
  • Insert a new panel via the Slash Menu (search "panel").

Relevant Docs:

import { BlockNoteSchema, defaultBlockSpecs } from "@blocknote/core";import {  filterSuggestionItems,  insertOrUpdateBlockForSlashMenu,} from "@blocknote/core/extensions";import "@blocknote/core/fonts/inter.css";import { BlockNoteView } from "@blocknote/mantine";import "@blocknote/mantine/style.css";import {  SuggestionMenuController,  getDefaultReactSlashMenuItems,  useCreateBlockNote,} from "@blocknote/react";import { useEffect, useState } from "react";import { RiChatQuoteLine } from "react-icons/ri";import { createPanel } from "./Panel";import "./styles.css";// Schema with the default blocks plus our custom Panel container block.const schema = BlockNoteSchema.create().extend({  blockSpecs: {    ...defaultBlockSpecs,    panel: createPanel(),  },});// Slash menu item to insert a Panel. Inserting one with no children fills// it with an empty paragraph, as `min` defaults to 1.const insertPanel = (editor: typeof schema.BlockNoteEditor) => ({  title: "Panel",  subtext: "Container block that wraps other blocks",  onItemClick: () =>    insertOrUpdateBlockForSlashMenu(editor, {      type: "panel",    }),  aliases: ["panel", "container", "callout", "alert", "note", "tip", "info"],  group: "Basic blocks",  icon: <RiChatQuoteLine />,});type AppBlock = (typeof schema.BlockNoteEditor)["document"][number];export default function App() {  const [blocks, setBlocks] = useState<AppBlock[]>([]);  const editor = useCreateBlockNote({    schema,    initialContent: [      {        type: "paragraph",        content: "Welcome! This demo shows the new container block kind.",      },      {        type: "panel",        props: { flavor: "tip" },        children: [          {            type: "paragraph",            content: "Panels can hold any block as their body.",          },          {            type: "paragraph",            content:              "Try pressing '/' inside this panel to add a heading or code block.",          },        ],      },      {        type: "paragraph",        content: "Press '/' anywhere to insert a new Panel.",      },      {        type: "paragraph",      },    ],  });  useEffect(() => setBlocks(editor.document), [editor]);  return (    <div className={"wrapper"}>      <div>BlockNote Editor:</div>      <div className={"item"}>        <BlockNoteView          editor={editor}          slashMenu={false}          onChange={() => {            setBlocks(editor.document);          }}        >          <SuggestionMenuController            triggerCharacter={"/"}            getItems={async (query) => {              const defaultItems = getDefaultReactSlashMenuItems(editor);              const lastBasicBlockIndex = defaultItems.findLastIndex(                (item) => item.group === "Basic blocks",              );              defaultItems.splice(                lastBasicBlockIndex + 1,                0,                insertPanel(editor),              );              return filterSuggestionItems(defaultItems, query);            }}          />        </BlockNoteView>      </div>      <div>Document JSON:</div>      <div className={"item bordered"}>        <pre>          <code>{JSON.stringify(blocks, null, 2)}</code>        </pre>      </div>    </div>  );}