ExamplesCustom SchemasContainer Block

Container Block

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.

The block declares the children config on BlockConfig. children: { min: 1, default: [{ type: "paragraph" }] } makes it a container: its child blocks mount into the element the render passes contentRef to, and live on block.children at runtime.

The callout's title demonstrates the complementary "string prop slot" pattern: a field that doesn't need rich text, comments, or multiplayer cursors can live in a plain string prop, edited through a regular <input> rendered inside the block (in a contentEditable={false} wrapper) and committed via editor.updateBlock. A field that is prose belongs in the block's own content: "inline" instead.

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.

Try it out:

  • Press the "/" key inside the callout's body and add a code block, heading, or list — anything goes.
  • Type a title into the title field — it's stored on block.props.title, not as document content.
  • Watch the JSON panel on the right update as you edit; the callout's children appear in block.children.
  • Insert a new callout via the Slash Menu (search "callout").

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 { createCallout } from "./Callout";import "./styles.css";// Schema with the default blocks plus our custom Callout container block.const schema = BlockNoteSchema.create().extend({  blockSpecs: {    ...defaultBlockSpecs,    callout: createCallout(),  },});// Slash menu item to insert a Callout. Because Callout is a container block,// inserting one with no children causes BlockNote to seed it with the block's// configured `children.default` (a single paragraph here).const insertCallout = (editor: typeof schema.BlockNoteEditor) => ({  title: "Callout",  subtext: "Container block that wraps other blocks",  onItemClick: () =>    insertOrUpdateBlockForSlashMenu(editor, {      type: "callout",    }),  aliases: ["callout", "container", "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: "callout",        props: { flavor: "tip" },        children: [          {            type: "paragraph",            content: "Callouts can hold any block as their body.",          },          {            type: "paragraph",            content:              "Try pressing '/' inside this callout to add a heading or code block.",          },        ],      },      {        type: "paragraph",        content: "Press '/' anywhere to insert a new Callout.",      },      {        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,                insertCallout(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>  );}