Alert Block with Full UX
In this example, we create a custom Alert block which is used to emphasize text, same as in the minimal Alert block example. However, in this example, we also add a command to insert the block via the Slash Menu, and an entry in the Formatting Toolbar's Block Type Select to change the current block to an Alert.
Try it out: Press the "/" key to open the Slash Menu and insert an Alert block! Or highlight text in a paragraph, then change the block type to an Alert using the Block Type Select in the Formatting Toolbar!
Relevant Docs:
- Minimal Alert Block Example
- Changing Slash Menu Items
- Changing Block Type Select Items
- Custom Blocks
- Editor Setup
import { defaultProps } from "@blocknote/core";import { createReactBlockSpec } from "@blocknote/react";import { Menu } from "@mantine/core";import { MdCancel, MdCheckCircle, MdError, MdInfo } from "react-icons/md";import "./styles.css";// The types of alerts that users can choose from.export const alertTypes = [ { title: "Warning", value: "warning", icon: MdError, color: "#e69819", backgroundColor: { light: "#fff6e6", dark: "#805d20", }, }, { title: "Error", value: "error", icon: MdCancel, color: "#d80d0d", backgroundColor: { light: "#ffe6e6", dark: "#802020", }, }, { title: "Info", value: "info", icon: MdInfo, color: "#507aff", backgroundColor: { light: "#e6ebff", dark: "#203380", }, }, { title: "Success", value: "success", icon: MdCheckCircle, color: "#0bc10b", backgroundColor: { light: "#e6ffe6", dark: "#208020", }, },] as const;// The Alert block.export const createAlert = createReactBlockSpec( { type: "alert", propSchema: { textAlignment: defaultProps.textAlignment, textColor: defaultProps.textColor, type: { default: "warning", values: ["warning", "error", "info", "success"], }, }, content: "inline", }, { render: (props) => { const alertType = alertTypes.find( (a) => a.value === props.block.props.type, )!; const Icon = alertType.icon; return ( <div className={"alert"} data-alert-type={props.block.props.type}> {/*Icon which opens a menu to choose the Alert type*/} <Menu withinPortal={false}> <Menu.Target> <div className={"alert-icon-wrapper"} contentEditable={false}> <Icon className={"alert-icon"} data-alert-icon-type={props.block.props.type} size={32} /> </div> </Menu.Target> {/*Dropdown to change the Alert type*/} <Menu.Dropdown> <Menu.Label>Alert Type</Menu.Label> <Menu.Divider /> {alertTypes.map((type) => { const ItemIcon = type.icon; return ( <Menu.Item key={type.value} leftSection={ <ItemIcon className={"alert-icon"} data-alert-icon-type={type.value} /> } onClick={() => props.editor.updateBlock(props.block, { type: "alert", props: { type: type.value }, }) } > {type.title} </Menu.Item> ); })} </Menu.Dropdown> </Menu> {/*Rich text field for user to type in*/} <div className={"inline-content"} ref={props.contentRef} /> </div> ); }, },);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 { BlockTypeSelectItem, FormattingToolbar, FormattingToolbarController, SuggestionMenuController, blockTypeSelectItems, getDefaultReactSlashMenuItems, useCreateBlockNote,} from "@blocknote/react";import { RiAlertFill } from "react-icons/ri";import { createAlert } from "./Alert";// Our schema with block specs, which contain the configs and implementations for// blocks that we want our editor to use.const schema = BlockNoteSchema.create().extend({ blockSpecs: { // Creates an instance of the Alert block and adds it to the schema. alert: createAlert(), },});// Slash menu item to insert an Alert blockconst insertAlert = (editor: typeof schema.BlockNoteEditor) => ({ title: "Alert", subtext: "Alert for emphasizing text", onItemClick: () => // If the block containing the text caret is empty, `insertOrUpdateBlock` // changes its type to the provided block. Otherwise, it inserts the new // block below and moves the text caret to it. We use this function with an // Alert block. insertOrUpdateBlockForSlashMenu(editor, { type: "alert", }), aliases: [ "alert", "notification", "emphasize", "warning", "error", "info", "success", ], group: "Basic blocks", icon: <RiAlertFill />,});export default function App() { // Creates a new editor instance. const editor = useCreateBlockNote({ schema, initialContent: [ { type: "paragraph", content: "Welcome to this demo!", }, { type: "alert", content: "This is an example alert", }, { type: "paragraph", content: "Press the '/' key to open the Slash Menu and add another", }, { type: "paragraph", content: "Or select some text to see the alert in the Formatting Toolbar's Block Type Select", }, { type: "paragraph", }, ], }); // Renders the editor instance. return ( <BlockNoteView editor={editor} formattingToolbar={false} slashMenu={false}> {/* Replaces the default Formatting Toolbar */} <FormattingToolbarController formattingToolbar={() => ( // Uses the default Formatting Toolbar. <FormattingToolbar // Sets the items in the Block Type Select. blockTypeSelectItems={[ // Gets the default Block Type Select items. ...blockTypeSelectItems(editor.dictionary), // Adds an item for the Alert block. { name: "Alert", type: "alert", icon: RiAlertFill, } satisfies BlockTypeSelectItem, ]} /> )} /> {/* Replaces the default Slash Menu. */} <SuggestionMenuController triggerCharacter={"/"} getItems={async (query) => { // Gets all default slash menu items. const defaultItems = getDefaultReactSlashMenuItems(editor); // Finds index of last item in "Basic blocks" group. const lastBasicBlockIndex = defaultItems.findLastIndex( (item) => item.group === "Basic blocks", ); // Inserts the Alert item as the last item in the "Basic blocks" group. defaultItems.splice(lastBasicBlockIndex + 1, 0, insertAlert(editor)); // Returns filtered items based on the query. return filterSuggestionItems(defaultItems, query); }} /> </BlockNoteView> );}.alert { display: flex; justify-content: center; align-items: center; flex-grow: 1; border-radius: 4px; min-height: 48px; padding: 4px;}.alert[data-alert-type="warning"] { background-color: #fff6e6;}.alert[data-alert-type="error"] { background-color: #ffe6e6;}.alert[data-alert-type="info"] { background-color: #e6ebff;}.alert[data-alert-type="success"] { background-color: #e6ffe6;}[data-color-scheme="dark"] .alert[data-alert-type="warning"] { background-color: #805d20;}[data-color-scheme="dark"] .alert[data-alert-type="error"] { background-color: #802020;}[data-color-scheme="dark"] .alert[data-alert-type="info"] { background-color: #203380;}[data-color-scheme="dark"] .alert[data-alert-type="success"] { background-color: #208020;}.alert-icon-wrapper { border-radius: 16px; display: flex; justify-content: center; align-items: center; margin-left: 12px; margin-right: 12px; height: 18px; width: 18px; user-select: none; cursor: pointer;}.alert-icon[data-alert-icon-type="warning"] { color: #e69819;}.alert-icon[data-alert-icon-type="error"] { color: #d80d0d;}.alert-icon[data-alert-icon-type="info"] { color: #507aff;}.alert-icon[data-alert-icon-type="success"] { color: #0bc10b;}.inline-content { flex-grow: 1;}