BlockNote DocsFeaturesCustom SchemasLayout Blocks

Layout Blocks

You can create custom blocks that contain other blocks, such as panels, tab sets, and column layouts. Take a look at the demo below, in which we add a custom panel to a BlockNote editor, as well as a custom Slash Menu item to insert it. Each panel can contain paragraphs, headings, lists, or any other blocks in the editor.

Creating a Layout Block

Use the createReactBlockSpec function to create a layout block, just like a Custom Block. For the panel below, we set content to "none" and layoutOnly to true, so the panel's child blocks go inside it:

import { createReactBlockSpec } from "@blocknote/react";

export const createPanel = createReactBlockSpec(
  {
    type: "panel",
    propSchema: {},
    content: "none",
    layoutOnly: true,
  },
  {
    renderFrame: (props) => (
      <div className="panel" ref={props.slotRef} />
    ),
  },
);

Block Config

The block config defines the content and child blocks your layout block can hold:

content: Must be "none" for a layout block: its node holds nothing but its child blocks. For a block with its own text and child blocks, see Combining Content and Child Blocks.

layoutOnly: Set to true to put the block's child blocks inside it. Without it, a block's child blocks are indented below it. A layout block accepts any block by default. You can also restrict it to specific layout block types, as explained in Restricting Children.

propSchema: Defines the layout block's props, just like for other custom blocks. Use these to customize its appearance or behavior.

Block Implementation

renderFrame: Your React component draws the layout block's box. A layout block has no content of its own, so it has no render, only this frame. Attach slotRef where the child blocks should appear. You can add icons, buttons, or other elements around it:

renderFrame: (props) => (
  <div className="panel">
    <span contentEditable={false}>💡</span>
    <div ref={props.slotRef} />
  </div>
),

You can style the component with CSS, just like any other React component:

.panel {
  display: flex;
  gap: 12px;
  padding: 16px;
  border-left: 4px solid #507aff;
  border-radius: 6px;
}

Adding Layout Blocks to the Editor

Add your layout block to a BlockNote schema:

import { BlockNoteSchema } from "@blocknote/core";
import { createPanel } from "./Panel";

const schema = BlockNoteSchema.create().extend({
  blockSpecs: {
    panel: createPanel(),
  },
});

You can then create an editor with this schema, as explained on the Custom Schemas page. Use children to set the blocks inside a panel:

import { useCreateBlockNote } from "@blocknote/react";

const editor = useCreateBlockNote({
  schema,
  initialContent: [
    {
      type: "panel",
      children: [
        { type: "heading", content: "Getting started" },
        { type: "paragraph", content: "Follow these steps to get set up." },
        { type: "checkListItem", content: "Create an account" },
      ],
    },
  ],
});

If you create a panel without specifying its children, it starts with an empty paragraph. To let users insert panels themselves, add a custom Slash Menu item, as shown in the demo.

Combining Content and Child Blocks

A block can have both its own text and child blocks. Use this for a question followed by hints, a checklist item with detailed instructions, a code sample followed by explanatory blocks, or an alert with a title. In the demo below, we extend the Alert block with a body: the block's text is the alert's title, and its child blocks are the body:

Set content to "inline" for rich text or "plain" for unstyled text. Every block with content can have child blocks, so there's nothing to declare for them. Use render for the block's own text, renderFrame to style that content and its child blocks together, and keyboard to keep the child blocks inside the block:

import { createReactBlockSpec } from "@blocknote/react";

export const createAlert = createReactBlockSpec(
  {
    type: "alert",
    propSchema: {
      type: {
        default: "warning",
        values: ["warning", "error", "info", "success"],
      },
    },
    content: "inline",
  },
  {
    experimental_keyboard: {
      enter: "into-children",
      childrenCanOutdent: false,
      emptyChildEnter: "exit-at-end",
    },
    render: (props) => (
      <div className="inline-content" ref={props.contentRef} />
    ),
    renderFrame: (props) => (
      <div className="alert-frame" data-alert-type={props.block.props.type}>
        <div ref={props.slotRef} />
      </div>
    ),
  },
);

render: Attach contentRef to the block's own editable text. With content: "inline", users can format it and add links, just like in a paragraph. In this example, it is the alert's title. The full example also adds the Alert's icon, which opens a menu to change the alert type.

For plain text, set content: "plain" and use a <pre> element to display line breaks and spacing:

render: (props) => <pre ref={props.contentRef} />,

The child blocks can still contain rich text, images, and other block types.

renderFrame: An optional React component for styling the block and its children together, such as giving the alert a shared background in the color of its type. It receives block, editor, and slotRef. Attach slotRef where the block's content and children should appear. You can use the block's props to customize the frame, add interactive controls, or return null to show the block without a frame.

experimental_keyboard: Without it, child blocks behave like any indented blocks. This API is experimental and may change. Here, enter: "into-children" makes Enter in the title add a first child block, childrenCanOutdent: false keeps Shift-Tab from moving blocks out of the alert, and emptyChildEnter: "exit-at-end" makes Enter in an empty last block leave the alert. See Custom Blocks for all keyboard settings.

A layout block with content: "none", like the panel above, has only renderFrame, and it always draws its frame.

Add alert: createAlert() to your schema, then use content for the title and children for the blocks inside it:

{
  type: "alert",
  content: "Before you start",
  children: [
    { type: "paragraph", content: "Make sure you have an account." },
  ],
}

Pressing Enter at the end of the title adds a paragraph to the body, and Enter in an empty last paragraph leaves the alert. Moving the alert moves its title and body together.

To add blocks to an existing layout block, see Inserting Blocks.

Restricting Children

For structured layouts, you can limit a layout block to specific layout block types, with these options in the block config:

children.allow: Accepts "blocks" (the default) or an array of layout block type names. You cannot list regular block types such as "paragraph" individually.

children.min: The minimum number of children, for an allow list only. Defaults to 1.

children.dissolve: Whether a layout block with too few children dissolves into what they hold, for an allow list only. Defaults to true, like a column list with one column left. Set false to keep the layout block and its empty children, like a tab set.

children.exclude: For a layout block that accepts any blocks, the layout block types it does not hold. Only layout block types can be excluded, not regular block types such as "paragraph".

placeable: Set to "namedOnly" to restrict a layout block to parents that name it in children.allow. Defaults to "anywhere".

These options need layoutOnly: true. Other blocks can always have child blocks of any type, so for them children can only be the default, { allow: "blocks" }.

For built-in column blocks, see Multi-Column Layouts.