Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
e9bb1b2
refactor(core): simplify the BlockInfo API into the single vocabulary…
nperez0111 Sep 4, 2026
70fc8f6
refactor(core): consolidate BlockInfo shape and insertion helpers
nperez0111 Sep 8, 2026
db08d7d
feat(container-blocks): unify containers, owned children, and frames
nperez0111 Sep 8, 2026
df78829
test(container-blocks): cover editing, frames, repair, and serialization
nperez0111 Sep 8, 2026
05bf572
docs(container-blocks): document the API and add panel and callout ex…
nperez0111 Sep 8, 2026
c0626cf
refactor(core): simplify container UI and block DOM lookup
nperez0111 Sep 8, 2026
f13b2b7
refactor(container-blocks): simplify rendering, editing, and exports
nperez0111 Sep 8, 2026
afa6358
fix(core): normalize container parsing and export
nperez0111 Sep 8, 2026
56a63d0
fix(tests): streamline Docker build inputs and cache invalidation
nperez0111 Sep 8, 2026
0bce75a
fix(container-blocks): simplify editing and support plain text
nperez0111 Sep 8, 2026
1707571
refactor(container-blocks): simplify implementation and examples
nperez0111 Sep 8, 2026
32fe072
fix(core): complete frame lifecycle and preserve split children
nperez0111 Sep 8, 2026
4f3ade8
fix: address container block review feedback
nperez0111 Sep 8, 2026
c4f0431
fix(core): unwrap a dissolving container in place (#3104)
YousefED Sep 24, 2026
c3ac08a
test(core): add behaviour tests for the built-in toggle blocks
YousefED Sep 29, 2026
cffcf6d
refactor(core): draw the built-in toggles with renderFrame
YousefED Sep 29, 2026
80f635a
feat(core)!: split keyboard behaviour from container structure
YousefED Sep 30, 2026
cd6bd89
feat(exporters): let block mappings place their children
YousefED Sep 30, 2026
4714c87
fix(core): apply block colors to the children of framed blocks
YousefED Sep 30, 2026
3bdd956
test(server-util): update the full HTML snapshot for block color attr…
YousefED Sep 30, 2026
8f0d594
fix: address review findings on keyboard settings and toggles
YousefED Sep 30, 2026
2e1f1ac
Merge branch 'main' into refactor/block-info-api
YousefED Sep 30, 2026
e206b7e
Merge branch 'refactor/block-info-api' into container-blocks/unified
YousefED Sep 30, 2026
9e72dc7
docs(core): note that ignoreFrameChromeMutations needs tests or removal
YousefED Sep 30, 2026
6caa68c
Merge branch 'container-blocks/unified' into container-blocks/editing…
YousefED Sep 30, 2026
8bc7dbb
feat(core): drop a block into a toggle (BLO-956)
YousefED Sep 30, 2026
2159763
fix(core): keep a block's id on Backspace below an alike empty block …
YousefED Sep 30, 2026
0b93f12
fix(core): drop as usual when the drop target is gone
YousefED Sep 30, 2026
bfd0c28
test: update block spec snapshot for meta.dropsIntoChildren
YousefED Sep 30, 2026
cb0315f
fix: one source for the block types that the menus offer (BLO-990, BL…
YousefED Sep 30, 2026
15714ef
refactor(core): rename the keyboard option to experimental_keyboard
YousefED Oct 9, 2026
5a13195
Merge remote-tracking branch 'origin/container-blocks/editing-rules' …
YousefED Oct 9, 2026
b95f09f
Merge remote-tracking branch 'origin/main' into refactor/block-info-api
YousefED Oct 9, 2026
c9a9f26
Merge branch 'container-blocks/unified' into container-blocks/editing…
YousefED Oct 9, 2026
f2a4eb3
Merge branch 'refactor/block-info-api' into container-blocks/unified
YousefED Oct 9, 2026
679148c
Merge branch 'container-blocks/editing-rules' into container-blocks/t…
YousefED Oct 9, 2026
352329e
Fold #3142 and #3143 into #3059: keyboard settings, container flag, t…
YousefED Oct 9, 2026
9778427
Merge main (#3051 squashed) into container-blocks/unified; content un…
YousefED Oct 9, 2026
372323b
Simplify nestBlock's slice, restrict children.min to allow lists, dro…
YousefED Oct 9, 2026
3b263ca
Keyboard, children and drag fixes from the second review round
YousefED Oct 10, 2026
7fb441e
Frame slot parsing, frames out of external HTML, one data-* attribute…
YousefED Oct 10, 2026
2f96214
Merge main (#3179 side menu hover, #3180) into container-blocks/unified
YousefED Oct 11, 2026
caf88d1
Containers draw only renderFrame; render never returns a frame (#3182)
YousefED Oct 12, 2026
4bfe08e
Frame views: one node view path for vanilla and React frames (#3184)
YousefED Oct 12, 2026
a9fe817
Workaround: measure a React container by its frame root
YousefED Oct 12, 2026
ab3c8ab
test(exporters): rely on the shared test document for child placement
YousefED Oct 13, 2026
0124596
fix(multi-column): throw when the drop target column is not in its co…
YousefED Oct 13, 2026
9414458
fix(core): drag previews of containers; dragged blocks keep their parent
YousefED Oct 13, 2026
b5fd8e3
fix(core): clearer block config errors; reject an unnamed namedOnly b…
YousefED Oct 13, 2026
a569895
feat(examples): container block examples; children.dissolve (#3140)
YousefED Oct 13, 2026
f020662
refactor!: rename container blocks to layout blocks (`layoutOnly`)
YousefED Oct 13, 2026
201738a
docs(examples): streamline the alert and panel examples
YousefED Oct 13, 2026
15712cc
small fix to stepper design
YousefED Oct 13, 2026
dfcf61e
docs: explain block frames in Custom Blocks; layoutOnly is a boolean
YousefED Oct 13, 2026
07048b3
fix: review findings for layout blocks
YousefED Oct 13, 2026
2149369
docs: layout blocks are a preview; simplify the comparison table
YousefED Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# these entries just stop the heavy/irrelevant trees from bloating the context.
**/node_modules
**/dist
**/.next
**/types
**/.vite
**/.vite-plus
Expand Down
153 changes: 147 additions & 6 deletions docs/content/docs/features/custom-schemas/custom-blocks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ type BlockConfig = {
type: string;
content: "inline" | "plain" | "none";
readonly propSchema: PropSchema;
layoutOnly?: boolean;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New properties aren't explained below. There is a callout which sort of does that, but clearer to just explain the properties.

children?:
| { allow?: "blocks"; exclude?: string[] }
| { allow: string[]; min?: number; dissolve?: boolean };
placeable?: "anywhere" | "namedOnly";
};
```

Expand Down Expand Up @@ -110,6 +115,15 @@ If you do not want the prop to have a default value, you can define it as an obj
Properties](/docs/features/blocks#default-block-properties)._
</Callout>

<Callout type="info">
_Every block can have child blocks, indented below it. You do not declare
them in the config. To draw a frame around a block and its child blocks, see
[Block Frames](#block-frames). `layoutOnly`, `children` and `placeable` are
only for layout blocks. A layout block only holds and arranges other blocks,
but has no content of its own, for example a panel or a tab set. See [Layout
Blocks](/docs/features/custom-schemas/layout-blocks)._
</Callout>

### Block Implementation (`ReactCustomBlockImplementation`)

The Block Implementation defines how the block should be rendered in the editor, and how it should be parsed from and converted to HTML.
Expand All @@ -121,25 +135,44 @@ type ReactCustomBlockImplementation = {
editor: BlockNoteEditor;
contentRef?: (node: HTMLElement | null) => void;
}>;
renderFrame?: React.FC<{
block: Block;
editor: BlockNoteEditor;
slotRef: (node: HTMLElement | null) => void;
}>;
toExternalHTML?: React.FC<{
block: Block;
editor: BlockNoteEditor;
contentRef?: (node: HTMLElement | null) => void;
contest: { nestingLevel: number };
childrenRef: (node: HTMLElement | null) => void;
context: { nestingLevel: number };
}>;
parse?: (element: HTMLElement) => PartialBlock["props"] | undefined;
parseContent?: (options: {
el: HTMLElement;
schema: Schema;
}) => Fragment | undefined;
runsBefore?: string[];
experimental_keyboard?:
| KeyboardSettings
| ((block: Block) => KeyboardSettings);
// KeyboardSettings: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO clean types

// enter?: "split" | "into-children" | "line-break";
// shiftEnter?: "line-break" | "same-as-enter";
// splitKeepsType?: boolean;
// splitKeepsChildren?: boolean;
// resetsTo?: { type: string; props?: Record<string, unknown> };
// emptyEnterResets?: boolean;
// emptyChildEnter?: "outdent" | "exit-at-end" | "stay";
// childrenCanOutdent?: boolean;
// }
meta?: {
hardBreakShortcut?: "shift+enter" | "enter" | "none";
selectable?: boolean;
fileBlockAccept?: string[];
code?: boolean;
defining?: boolean;
isolating?: boolean;
draggable?: boolean;
highlight?: (block: Block) => string | undefined;
};
};
Expand All @@ -153,9 +186,14 @@ type ReactCustomBlockImplementation = {

- `contentRef:` A React `ref` you can use to mark which element in your block is editable. This is only available if your block config contains `content: "inline"` or `content: "plain"`.

`toExternalHTML?:` This component is used whenever the block is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` for the HTML conversion. Takes the same props as `render` and an additional `context` prop, which is. an object with the following attributes:
`renderFrame?:` A component that draws a frame around the block's content and its child blocks. See [Block Frames](#block-frames).

`toExternalHTML?:` This component is used whenever the block is being exported to HTML for use outside BlockNote, for example when copying it to the clipboard. If it's not defined, BlockNote will just use `render` (and `renderFrame`, if the block has one) for the HTML conversion. Takes the same props as `render`, and two more:

- `childrenRef:` A React `ref` that marks where the block's child blocks go, for example inside a `<details>` element. Without it, the child blocks follow the block.

- `nestingLevel`: How deep the block being exported in nested inside other blocks. 0 means it's at the top level of the document.
- `context:` An object with the following attributes:
- `nestingLevel`: How deep the block being exported is nested inside other blocks. 0 means it's at the top level of the document.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was nestingLevel added as part of this PR? Is it related to layout blocks?


<Callout type="info">
_Note that your component passed to `toExternalHTML` is rendered and
Expand All @@ -171,9 +209,27 @@ type ReactCustomBlockImplementation = {

`runsBefore?:` If this block has parsing or extensions that need to be given priority over any other blocks, you can pass their `type`s in an array here.

`meta?:` An object for setting various generic properties of the block.
`experimental_keyboard?:` How the keyboard treats the block and its children. This API is experimental and may change. Give only the settings that differ from the defaults. To make settings depend on the block's props, give a function that gets the block and returns them instead.

- `enter?:` What Enter does in the block's content. `"split"` (default) splits the block, moving the text after the caret into a new block below. `"into-children"` moves it into a new first child instead. `"line-break"` inserts a line break, and makes Shift-Enter do the same. For `content: "plain"` blocks (which can't hold hard break nodes), a line break is a literal newline (`"\n"`).

- `shiftEnter?:` What Shift-Enter does in the block's content: `"line-break"` (default) or `"same-as-enter"`.

- `splitKeepsType?:` Whether the block created by splitting this one with Enter has the same type, as in lists. Defaults to `false`.

- `splitKeepsChildren?:` Whether the block's children stay with it when Enter splits it. Otherwise they follow the text after the caret into the new block, as for nested blocks, so the new block takes the block's place above them. Defaults to `true` when `enter` is `"into-children"` (the children are the block's body), otherwise `false`; a closed toggle sets it to keep its hidden children.

- `resetsTo?:` What the block turns into when it's reset, keeping its content and children. Backspace at the start of the block always resets it. A `type`, and `props` to merge into the block's props. Defaults to `{ type: "paragraph" }`.

- `emptyEnterResets?:` Whether Enter in the empty block resets it too, as when an empty list item turns into a paragraph. Defaults to `false`.

- `emptyChildEnter?:` What Enter does in an empty child of this block. `"outdent"` (default) outdents any empty child. `"exit-at-end"` (default for layout blocks) moves an empty last child out to after the block, and adds a new child after any other empty child. `"stay"` always adds a new child after it.

- `hardBreakShortcut?:` Defines which keyboard shortcut should be used to insert a hard break into the block's inline content. Defaults to `"shift+enter"`. For `content: "plain"` blocks (which can't hold hard break nodes), the shortcut inserts a literal newline (`"\n"`) instead.
- `childrenCanOutdent?:` Whether the block's children can be outdented out of it with Shift-Tab, or by Enter or Backspace in an empty or nested child. Defaults to `true`, or `false` for layout blocks, whose children can never be outdented.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use unindent/unnest rather than outdent, so we should stick to that naming convention


When settings meet, Enter at the start of non-empty content always inserts an empty block above it, and resetting an empty block comes before `enter: "into-children"`.

`meta?:` An object for setting various generic properties of the block.

- `selectable?:` Can be set to false in order to make the block non-selectable, both using the mouse and keyboard. This also helps with being able to select non-editable content within the block. Should only be set to false when `content` is `none` and defaults to true.

Expand All @@ -185,6 +241,8 @@ type ReactCustomBlockImplementation = {

- `isolating?:` Whether this block is [isolating](https://prosemirror.net/docs/ref/#model.NodeSpec.isolating).

- `draggable?:` Whether the block gets a side menu drag handle. Defaults to `true`, or `false` for a layout block that can only be placed inside a specific parent (`placeable: "namedOnly"`).

- `highlight?:` A callback that returns the language the block's text should be syntax-highlighted as (or `undefined` for no highlighting). Highlighting only takes effect when the [syntax highlighting extension](/docs/features/blocks/code-blocks#syntax-highlighting) is added to the editor.

### Block Extensions
Expand Down Expand Up @@ -257,3 +315,86 @@ You can then instantiate your editor with this custom schema, as explained on th
Now you know how to create a custom block and add it to your editor. However, users don't have a way of creating instances of it to their documents.

To fix this, it's recommended to implement a [command to insert your custom in the Slash Menu](/docs/react/components/suggestion-menus#changing-slash-menu-items), and [an item for it in the Block Type Select](/docs/react/components/formatting-toolbar)

## Block Frames

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! All clear


A block can have child blocks. By default, BlockNote shows the child blocks indented below the block. Use `renderFrame` to draw one frame around the block's content and its child blocks, for example one background for both.

In the demo below, we give the Alert block from the demo above a title and a body. The alert's text is its title, and its child blocks are its body. The frame gives the title and the body the background color of the alert type:

<Example name="custom-schema/alert-block-with-title-and-body" />

The block config does not change. A block with content (`content: "inline"` or `"plain"`) can always have child blocks, so you do not declare them. We add `renderFrame` and `experimental_keyboard` to the block implementation:

```tsx
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",
},
// Draws the title row, the same as in the Alert demo.
render: (props) => {
...
},
renderFrame: (props) => (
<div className="alert-frame" data-alert-type={props.block.props.type}>
<div ref={props.slotRef} />
</div>
),
},
);
```

`render:` Draws only the block's content, here the alert's title row. It does not change when you add a frame.

`renderFrame:` A React component that draws the frame. It takes three React props:

- `block:` The block. Use its props to change the frame, for example to show the color of the alert type.

- `editor:` The BlockNote editor instance that the block is in.

- `slotRef:` A React `ref`. Attach it to the element that holds the block's content and its child blocks. You can add other elements around this element, for example icons or buttons. Set `contentEditable={false}` on them.

To show the block without a frame, return `null`. For example, you can use a prop to turn the frame on and off.

`experimental_keyboard:` Without these settings, the child blocks behave like all other nested blocks. Here, they keep the body in the alert:

- `enter: "into-children"`: Enter in the title adds a block at the start of the body.
- `childrenCanOutdent: false`: Shift-Tab does not move blocks out of the alert.
- `emptyChildEnter: "exit-at-end"`: Enter in an empty last block of the body moves that block out of the alert.

See [`experimental_keyboard`](#block-implementation-reactcustomblockimplementation) above for all settings.

Use `content` for the title and `children` for the body:

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

When the user moves the alert, its title and its body move together.

<Callout type="info">
_A block with a frame is different from a [layout
block](/docs/features/custom-schemas/layout-blocks). A layout block has no
content of its own. It only holds child blocks, for example a panel or a tab
set. See [Layout Blocks and Block
Frames](/docs/features/custom-schemas/layout-blocks#layout-blocks-and-block-frames)._
</Callout>
Loading
Loading