ExamplesCustom SchemasTable Built From Container Blocks

Table Built From Container Blocks

In this example, we rebuild BlockNote's table as four container blocks: table, tableRow, tableCell, and tableHeader. There is no prosemirror-tables and no special "table" content type. A table is a container of rows, a row is a container of cells, and a cell is a container of arbitrary blocks, so cells can hold lists, headings, images, or even nested tables. The JSON shape is the same children array every other block uses.

Cells declare boundary: "sealed", which makes them behave like compartments: Backspace, Delete, and arrow keys never implicitly move content or the caret across a cell's edge, and Enter adds another block inside the cell. Header cells are a distinct block type rather than table metadata, so toggling the header row is just updateBlock with a new type. All structural operations, from adding and removing rows and columns to Tab-to-next-cell, are plain calls to the public block manipulation API: insertBlocks, removeBlocks, updateBlock, getParentBlock, and setTextCursorPosition.

Try it out:

  • Press Tab / Shift-Tab to move between cells. Tab in the last cell adds a new row.
  • Press Enter inside a cell to stack more blocks in it, or "/" to add a list or heading.
  • Hover the table to reveal the row/column controls, and watch the JSON panel update.

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 { TbTable } from "react-icons/tb";import {  createTable,  createTableCell,  createTableHeader,  createTableRow,} from "./Table";import "./styles.css";// Drop the built-in table (the one with the special `"table"` content type)// and replace it with our container-based implementation under the same// `table` type name. The specs are passed to `create` rather than `extend`,// as `extend` can only add new block types, not replace existing ones.const { table: _defaultTable, ...remainingBlockSpecs } = defaultBlockSpecs;const schema = BlockNoteSchema.create({  blockSpecs: {    ...remainingBlockSpecs,    table: createTable(),    tableRow: createTableRow(),    tableCell: createTableCell(),    tableHeader: createTableHeader(),  },});// Inserting a table with no explicit children seeds it from the block's// configured `children.default`: a 3-column table with a header row.const insertTable = (editor: typeof schema.BlockNoteEditor) => ({  title: "Table",  subtext: "Table built from container blocks",  onItemClick: () =>    insertOrUpdateBlockForSlashMenu(editor, {      type: "table",    }),  aliases: ["table", "grid", "cells"],  group: "Basic blocks",  icon: <TbTable />,});type AppBlock = (typeof schema.BlockNoteEditor)["document"][number];export default function App() {  const [blocks, setBlocks] = useState<AppBlock[]>([]);  const editor = useCreateBlockNote({    schema,    initialContent: [      {        type: "paragraph",        content:          "This table is built entirely from container blocks, with no special table content type.",      },      {        type: "table",        children: [          {            type: "tableRow",            children: [              {                type: "tableHeader",                children: [{ type: "paragraph", content: "Name" }],              },              {                type: "tableHeader",                children: [{ type: "paragraph", content: "Notes" }],              },            ],          },          {            type: "tableRow",            children: [              {                type: "tableCell",                children: [{ type: "paragraph", content: "Alice" }],              },              {                type: "tableCell",                children: [                  {                    type: "paragraph",                    content: "Cells hold any blocks:",                  },                  {                    type: "bulletListItem",                    content: "lists,",                  },                  {                    type: "bulletListItem",                    content: "headings, images…",                  },                ],              },            ],          },          {            type: "tableRow",            children: [              {                type: "tableCell",                children: [{ type: "paragraph", content: "Bob" }],              },              {                type: "tableCell",                children: [                  {                    type: "paragraph",                    content: "Tab / Shift-Tab move between cells.",                  },                ],              },            ],          },        ],      },      {        type: "paragraph",        content:          "Tab in the last cell adds a row. Press '/' to insert a new table.",      },      {        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) => {              // Swap the built-in Table item (which inserts the old              // `tableContent` shape) for one that inserts our container              // table.              const defaultItems = getDefaultReactSlashMenuItems(editor).filter(                (item) => item.title !== "Table",              );              const lastBasicBlockIndex = defaultItems.findLastIndex(                (item) => item.group === "Basic blocks",              );              defaultItems.splice(                lastBasicBlockIndex + 1,                0,                insertTable(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>  );}