Custom Block Types
In addition to the default block types that BlockNote offers, you can also make your own custom blocks using React components. Take a look at the demo below, in which we add a custom alert block to a BlockNote editor, as well as a custom Slash Menu Item to insert it.
Creating a Custom Block Type
Use the createReactBlockSpec
function to create a custom block type. This function takes three arguments:
function createReactBlockSpec(
blockConfig: CustomBlockConfig,
blockImplementation: ReactCustomBlockImplementation,
extensions?: BlockNoteExtension[],
): (options? BlockOptions) => BlockSpec;
It returns a function that you can call to create an instance of your custom block, or a BlockSpec
. This BlockSpec
then gets passed into your BlockNote schema to add the block to the editor. This function may also take arbitrary options, which you can find out more about below.
Let's look at our custom alert block from the demo, and go over everything we pass to createReactBlockSpec
:
const createAlert = createReactBlockSpec(
{
type: "alert",
propSchema: {
textAlignment: defaultProps.textAlignment,
textColor: defaultProps.textColor,
type: {
default: "warning",
values: ["warning", "error", "info", "success"],
},
},
content: "inline",
},
{
render: (props) => {
...
},
}
);
Block Config (CustomBlockConfig
)
The Block Config describes the shape of your custom blocks. Use it to specify the type, properties (props) and content your custom blocks should support:
type BlockConfig = {
type: string;
content: "inline" | "none";
readonly propSchema: PropSchema;
};
type:
Defines the identifier of the custom block.
content:
inline
if your custom block should support rich text content, none
if not.
In the alert demo, we want the user to be able to type text in our alert, so
we set content
to "inline"
.
propSchema:
The PropSchema
specifies the props that the block supports. Block props (properties) are data stored with your Block in the document, and can be used to customize its appearance or behavior.
type PropSchema<PrimitiveType extends "boolean" | "number" | "string"> = Record<
string,
| {
default: PrimitiveType;
values?: PrimitiveType[];
}
| {
default: undefined;
type: PrimitiveType;
values?: PrimitiveType[];
}
>;
[key: string]
is the name of the prop. If you want it to have a default value, it should be defined as an object with the following properties:
-
default:
Specifies the prop's default value, from whichPrimitiveType
is inferred. -
values?:
Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. Ifvalues
is not defined, BlockNote assumes the prop can be any value ofPrimitiveType
.
If you do not want the prop to have a default value, you can define it as an object with the following properties:
-
default:
Leftundefined
, as there is no default value. -
type:
SpecifiesPrimitiveType
that the prop can be set to, since the default value isundefined
and cannot be inferred from. -
values?:
Specifies an array of values that the prop can take, for example, to limit the value to a list of pre-defined strings. Ifvalues
is not defined, BlockNote assumes the prop can be any value ofPrimitiveType
.
In the alert demo, we add a type
prop for the type of alert that we want
(warning / error / info / success). We also want basic styling options, so we
add text alignment and text color from the Default Block
Properties.
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.
type ReactCustomBlockImplementation = {
render: React.FC<{
block: Block;
editor: BlockNoteEditor;
contentRef?: (node: HTMLElement | null) => void;
}>;
toExternalHTML?: React.FC<{
block: Block;
editor: BlockNoteEditor;
contentRef?: (node: HTMLElement | null) => void;
}>;
parse?: (element: HTMLElement) => PartialBlock["props"] | undefined;
runsBefore?: string[];
meta?: {
hardBreakShortcut?: "shift+enter" | "enter" | "none";
selectable?: boolean;
fileBlockAccept?: string[];
code?: boolean;
defining?: boolean;
isolating?: boolean;
};
};
render:
This is your React component which defines how your custom block should be rendered in the editor, and takes three React props:
-
block:
The block that should be rendered. Its type and props will match the type and PropSchema defined in the Block Config. -
editor:
The BlockNote editor instance that the block is in. -
contentRef:
A Reactref
you can use to mark which element in your block is editable, this is only available if your block config containscontent: "inline"
.
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
.
Note that your component passed to toExternalHTML
is rendered and
serialized in a separate React root, which means you can't use hooks that rely
on React Contexts.
parse?:
The parse
function defines how to parse HTML content into your block, for example when pasting contents from the clipboard. If the element should be parsed into your custom block, you return the props that the block should be given. Otherwise, return undefined
. Takes a single argument:
element
: The HTML element that's being parsed.
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.
-
hardBreakShortcut?:
Defines which keyboard shortcut should be used to insert a hard break into the block's inline content. Defaults to"shift+enter"
. -
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 whencontent
isnone
and defaults to true. -
fileBlockAccept?:
For custom file blocks, this specifies which MIME types are accepted when uploading a file. All file blocks should specify this property, and should use aFileBlockWrapper
/ResizableFileBlockWrapper
component in theirrender
functions (see next subsection). -
code?:
Whether this block contains code. -
defining?:
Whether this block is defining. -
isolating?:
Whether this block is isolating.
Block Extensions
While the example on this page doesn't use it, createReactBlockSpec
takes a third, optional argument extensions
. This is for adding editor extensions
that are specific to the block, which you can find out more about here.
Block extensions are typically things like e.g. adding keyboard shortcuts to change the current block type to a custom block. For a table of contents block, an extension could also add a ProseMirror plugin to scan for headings to put in the ToC.
Block Config Options
In some cases, you may want to have a customizable block config. For example, you may want to be able to have a code block with syntax highlighting for either web or embedded code, or a heading block with a flexible number of heading levels. You can use the same API for this use case, with some minor changes:
// Arbitrary options that your block can take, e.g. number of heading levels or
// available code syntax highlight languages.
type CustomBlockConfigOptions = {
...
}
const createCustomBlock = createReactBlockSpec(
createBlockConfig((options: CustomBlockConfigOptions) => ({
type: "customBlock"
propSchema: ...,
content: ...,
})),
(options: CustomBlockConfigOptions) => ({
render: ...,
...
})
)
const options: CustomBlockConfigOptions = {
...
};
const schema = BlockNoteSchema.create().extend({
blockSpecs: {
// Creates an instance of the custom block and adds it to the schema.
customBlock: createCustomBlock(options),
},
});
You can see that instead of passing plain objects for the config and implementation, we instead pass functions. These take the block options as an argument, and return the config and implementation objects respectively. Additionally, the function for creating the config is wrapped in a createBlockConfig
function.
Also notice that for the example on this page, we create a new Alert block instance by simply calling createAlert()
with no arguments. When a custom block takes options though, you can pass them in when creating an instance, as shown above.
To see a full example of block options being used, check out the built-in heading block.
Adding Custom Blocks to the Editor
Finally, create a BlockNoteSchema using the definition of your custom blocks:
const schema = BlockNoteSchema.create({
blockSpecs: {
// enable the default blocks if desired
...defaultBlockSpecs,
// Add your own custom blocks:
alert: createAlert(),
},
});
You can then instantiate your editor with this custom schema, as explained on the Custom Schemas page.
Improving the User Experience
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, and an item for it in the Block Type Select