ExamplesCustom SchemasCard with Ordered Children

Card with Ordered Children

In this example, we create a Card container block whose children are an ordered sequence: a cardHeader followed by a cardBody, exactly one of each. The sequence compiles to the ProseMirror content expression cardHeader cardBody, so the order is enforced by the document schema rather than by a repair pass — a card built as [body, header] is rejected before it can reach the document.

The header is a content-bearing container: it has content: "inline" for its rich-text title and children for optional blocks beneath it, both placed with the single contentRef.

Try it out: click the button below the editor to insert a card the wrong way round, and read the error the schema gives back.

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 { useState } from "react";import { RiLayoutRowLine } from "react-icons/ri";import { createCard, createCardBody, createCardHeader } from "./Card";import "./styles.css";// Schema with the default blocks plus the three blocks the card is made of.const schema = BlockNoteSchema.create().extend({  blockSpecs: {    ...defaultBlockSpecs,    card: createCard(),    cardHeader: createCardHeader(),    cardBody: createCardBody(),  },});// Slash menu item to insert a card. It passes no children: the card's content// expression (`cardHeader cardBody`) tells BlockNote exactly what to fill it// with, so a container can never be created in an invalid state.const insertCard = (editor: typeof schema.BlockNoteEditor) => ({  title: "Card",  subtext: "A header followed by a body, in that order",  onItemClick: () =>    insertOrUpdateBlockForSlashMenu(editor, {      type: "card",    }),  aliases: ["card", "panel", "sequence"],  group: "Basic blocks",  icon: <RiLayoutRowLine />,});export default function App() {  // What ProseMirror says when a card is built the wrong way round.  const [rejection, setRejection] = useState<string | undefined>();  const editor = useCreateBlockNote({    schema,    initialContent: [      {        type: "paragraph",        content:          "A card's children are an ordered sequence: a header, then a body.",      },      {        type: "card",        props: { accent: "violet" },        children: [          {            type: "cardHeader",            content: "Ordered children",          },          {            type: "cardBody",            children: [              {                type: "paragraph",                content:                  "The header holds rich text of its own — try bolding a word in it.",              },              {                type: "paragraph",                content:                  "The body holds any blocks. Press '/' here to add a heading or a list.",              },            ],          },        ],      },      {        type: "paragraph",        content: "Press '/' anywhere to insert another card.",      },      {        type: "paragraph",      },    ],  });  // Building a card as [body, header] is not something BlockNote has to check  // for: the block simply doesn't fit the schema, and inserting it throws.  const insertReversedCard = () => {    try {      editor.insertBlocks(        [          {            type: "card",            children: [              { type: "cardBody", children: [{ type: "paragraph" }] },              { type: "cardHeader" },            ],          },        ],        editor.document[0],        "before",      );      setRejection("Unexpectedly accepted!");    } catch (error) {      setRejection((error as Error).message);    }  };  return (    <div className={"wrapper"}>      <div className={"item"}>        <BlockNoteView editor={editor} slashMenu={false}>          <SuggestionMenuController            triggerCharacter={"/"}            getItems={async (query) => {              const defaultItems = getDefaultReactSlashMenuItems(editor);              const lastBasicBlockIndex = defaultItems.findLastIndex(                (item) => item.group === "Basic blocks",              );              defaultItems.splice(                lastBasicBlockIndex + 1,                0,                insertCard(editor),              );              return filterSuggestionItems(defaultItems, query);            }}          />        </BlockNoteView>      </div>      <div className={"rejection"}>        <button type={"button"} onClick={insertReversedCard}>          Try inserting a card as [body, header]        </button>        {rejection && <code>{rejection}</code>}      </div>    </div>  );}