Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/content/docs/getting-started/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ For more information about the `useCreateBlockNote` hook and the `BlockNoteView`

## Mobile compatibility

If your app is used on touch devices, it's good practice to add `interactive-widget=resizes-content` to your page's viewport meta tag:
On touch devices, BlockNote's UI adapts on its own: specifically, the [Formatting Toolbar](/docs/react/components/formatting-toolbar#mobile-formatting-toolbar) moves above the on-screen keyboard.
Two things are still worth doing in your app to ensure smooth positioning of the toolbar in relation to the on-screen keyboard:

First, we recommend adding `interactive-widget=resizes-content` to your page's viewport meta tag:

```html
<meta
Expand All @@ -78,7 +81,9 @@ If your app is used on touch devices, it's good practice to add `interactive-wid
/>
```

This isn't specific to BlockNote - it mitigates undesired behaviour that some browsers have when the virtual keyboard is open. For more on BlockNote's mobile UX, see the [mobile Formatting Toolbar](/docs/react/components/formatting-toolbar#mobile-formatting-toolbar).
This isn't specific to BlockNote: it tells the browser to lay out the page in the space above the keyboard, instead of leaving part of it behind the keyboard. That lets BlockNote position the toolbar there reliably and without jitter.

Second, iOS doesn't support that tag, so scrolling can still feel choppy there. We recommend the [scroll container](/docs/react/components/formatting-toolbar#browser-limitations) layout to fix this.

## Next steps

Expand Down
4 changes: 3 additions & 1 deletion docs/content/docs/getting-started/vanilla-js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ Now, you'll have a plain BlockNote instance on your page. However, it's missing

## Creating your own UI elements

Because you can't use the built-in React [UI Components](/docs/react/components), you'll need to create and register your own UI elements.
Because you can't use the built-in React [UI Components](/docs/react/components), you'll need to create your own UI elements.

If an element you create lives outside the editor's own DOM, call `editor.registerPortalElement(element)` while it's on the page and `editor.unregisterPortalElement(element)` when you remove it. Without that, focus moving into it counts as the user leaving the editor.

Each UI element is backed by an [extension](/docs/features/extensions). You can retrieve an extension instance from the editor with `editor.getExtension(...)`, and each one exposes a store that holds its current state (visibility, position, and any element-specific data). A store is a small observable container with these primary members: `state` to read the current value, `setState` to update it, and `subscribe` to be notified of changes. The available UI element extensions are:

Expand Down
18 changes: 10 additions & 8 deletions docs/content/docs/react/components/formatting-toolbar.mdx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
title: Formatting Toolbar
description: The Formatting Toolbar appears whenever you highlight text in the editor.
description: The Formatting Toolbar appears whenever you select content in the editor on desktop, or above the virtual keyboard while the editor is focused on touch devices.
---

# Formatting Toolbar

The Formatting Toolbar appears whenever you highlight text in the editor.
The Formatting Toolbar appears above or below highlighted text on desktop. On touch devices with the on-screen keyboard open, it appears above the keyboard instead.

<ThemedImage
src={{
Expand All @@ -27,7 +27,7 @@ We first define our custom `BlueButton`. The `useComponentsContext` hook gets al

We use the `FormattingToolbar` component to create a custom Formatting Toolbar. By specifying its children, we can replace the default buttons in the toolbar with our own.

This custom Formatting Toolbar is passed to a `FormattingToolbarController`, which controls its position and visibility (above or below the highlighted text).
This custom Formatting Toolbar is passed to a `FormattingToolbarController`, which controls its position and visibility and automatically switches between desktop and mobile layouts. The custom toolbar is used in both layouts.

Setting `formattingToolbar={false}` on `BlockNoteView` tells BlockNote not to show the default Formatting Toolbar.

Expand All @@ -41,22 +41,24 @@ Here, we use the `FormattingToolbar` component but keep the default buttons (we

## Mobile Formatting Toolbar

On touch devices, BlockNote's default UI replaces the floating Formatting Toolbar with a mobile Formatting Toolbar that sits just above the on-screen keyboard. It shows the same items as the regular Formatting Toolbar and is enabled by default - there's nothing to set up. Open any of the examples above on a phone to see it.
On touch devices, BlockNote shows the mobile Formatting Toolbar above the on-screen keyboard while it's open and the editor is focused, rather than floating around the selected content. It has the same items as the regular Formatting Toolbar and is enabled by default - there's nothing to set up. Open any of the examples above on a phone to see it.

### Browser limitations

Mobile browsers vary in how they handle the on-screen keyboard, which can affect the toolbar. Before anything else, make sure your page has the viewport meta tag from the [mobile compatibility guide](/docs/getting-started#mobile-compatibility).
Mobile browsers vary in how they handle the on-screen keyboard, which can affect the toolbar. This assumes your page already has the viewport meta tag from the [mobile compatibility guide](/docs/getting-started#mobile-compatibility) - add it first if you haven't.

Most notably, iOS doesn't support the aforementioned viewport meta tag, which can result in jitter/lag when scrolling. There is however a workaround for this, which is why you won't see any choppiness in any of the examples here.
Most notably, iOS doesn't support the aforementioned viewport meta tag, which can result in jitter/lag when scrolling.

The workaround is to wrap all of your app's content in a single element with the `bn-scroll-container` class:

```tsx
<div className="bn-scroll-container">{/* nav, editor, page content... */}</div>
```

This element must be a direct child of `<body>` and cover its full area. BlockNote will keep its size in sync with the [visual viewport](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport) and render the Formatting Toolbar outside of it, eliminating any jitter/lag.
We recommend placing this element directly inside `<body>`, wrapping all of your page content. BlockNote will keep its size in sync with the [visual viewport](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport) and render the Formatting Toolbar outside of it, eliminating any jitter/lag.

<Callout type="warning">
Your app should only ever have a single `bn-scroll-container` element. It's pinned to the visual viewport with `position: fixed`, so multiple containers would overlap each other. Wrap all your scrollable page content in one.
Your app should only ever have a single `bn-scroll-container` element. Wrap all your scrollable page content in one.
</Callout>

The standalone [Mobile Formatting Toolbar example](https://playground.blocknotejs.org/ui-components/mobile-formatting-toolbar?hideMenu=true) lets you switch between document scrolling and the pinned scroll container to compare their behavior.
4 changes: 3 additions & 1 deletion docs/content/docs/react/components/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ By default, the floating components (formatting toolbar, side menu, slash menu,
/>
```

Keys mirror the default UI flags (`formattingToolbar`, `linkToolbar`, `slashMenu`, `emojiPicker`, `sideMenu`, `filePanel`, `tableHandles`, `comments`). Manually-mounted Controllers also accept a `portalElement` prop that takes precedence over the map. All keys, including `default`, update reactively. See the [Portal Targets example](/examples/ui-components/portal-elements).
Keys mirror the default UI flags (`formattingToolbar`, `linkToolbar`, `slashMenu`, `emojiPicker`, `sideMenu`, `filePanel`, `tableHandles`, `comments`, `attributionTooltip`). Manually-mounted Controllers also accept a `portalElement` prop that takes precedence over the map. All keys, including `default`, update reactively. See the [Portal Targets example](/examples/ui-components/portal-elements).

The [mobile Formatting Toolbar](/docs/react/components/formatting-toolbar#mobile-formatting-toolbar) always portals to `document.body`, including its menus and popovers. It does not use the configured formatting-toolbar or default target.

When a target sits outside the editor's DOM (like `document.body`), BlockNote automatically renders a themed wrapper element inside it, so floating UI keeps the editor's styling and theming wherever it's portalled.
27 changes: 27 additions & 0 deletions docs/content/docs/react/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,33 @@ declare function useEditorSelectionChange(
): BlockNoteEditor;
```

### useEditorFocus

The `useEditorFocus` hook returns a boolean and re-renders your component when the editor gains or loses focus. By default, it tracks the content area. Pass `includeEditorUI: true` to keep it `true` while focus moves into the editor's toolbars, menus, or popovers.

```tsx twoslash
/**
* See the [Editor API reference](/docs/reference/editor/overview) for more details
*/
type BlockNoteEditor = object;
/**
* This hook tracks whether the editor is focused.
*/
// ---cut---
declare function useEditorFocus(
options?: {
/**
* Include focus within the editor's toolbars, menus, and popovers.
* Defaults to false.
*/
includeEditorUI?: boolean;
},
editor?: BlockNoteEditor,
): boolean;
```

The options and editor arguments are optional. When called inside `BlockNoteView`, the hook can use the editor from context. To run a side effect on focus changes, subscribe to [`editor.onFocusChange`](/docs/reference/editor/events#onfocuschange).

## Next Steps

The editor is now ready to use! Start typing and explore the various block types and formatting options available in the toolbar.
Expand Down
4 changes: 3 additions & 1 deletion docs/content/docs/react/styling-theming/overriding-css.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ BlockNote uses classes with the `bn-` prefix to style editor elements. Here are

#### Editor Structure

- `.bn-root`: Container class both the floating menus / toolbars and the editor
- `.bn-root`: Themed root for the editor or portalled UI. An editor can have multiple roots.
- `.bn-container`: Container around `.bn-editor`
- `.bn-editor`: Main editor element (the "contenteditable").
- `.bn-block`: Individual block element (including nested).
Expand All @@ -36,6 +36,8 @@ BlockNote uses classes with the `bn-` prefix to style editor elements. Here are
- `.bn-drag-handle-menu`: Drag handle menu.
- `.bn-suggestion-menu`: Suggestion menu.

Menus and popovers may be portalled outside the toolbar or button that opened them. Target their own classes under `.bn-root` rather than relying on a toolbar or `.bn-container` ancestor. See [Configuring Portal Targets](/docs/react/components#configuring-portal-targets).

### BlockNote CSS Attributes

BlockNote uses data attributes to target specific block types and properties:
Expand Down
21 changes: 21 additions & 0 deletions docs/content/docs/reference/editor/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The editor emits events for:
- **Editor lifecycle** - When the editor is created, mounted, unmounted, etc.
- **Content changes** - When blocks are inserted, updated, or deleted
- **Selection changes** - When the cursor position or selection changes
- **Focus changes** - When focus enters or leaves the editor

## `onMount`

Expand Down Expand Up @@ -52,6 +53,26 @@ editor.onSelectionChange((editor) => {
});
```

## `onFocusChange`

The `onFocusChange` callback receives the editor and a context containing `focused` (a boolean) and `event` (the triggering DOM `FocusEvent`). By default, it reports focus entering or leaving the content area.

Pass `includeEditorUI: true` to count focus within the editor's toolbars, menus, and popovers too. In this mode, the callback runs only when that combined state changes, allowing focus to settle so moving from the content area into a popover input does not report a blur.

```typescript
const unsubscribe = editor.onFocusChange(
(editor, { focused }) => {
console.log("Interacting with editor:", focused);
},
{ includeEditorUI: true },
);

// When you no longer need the listener:
unsubscribe();
```

For the current focus state, use [`editor.isFocused()`](/docs/reference/editor/overview#focus). In React, use [`useEditorFocus`](/docs/react/overview#useeditorfocus) when focus determines what to render.

## `onChange`

The `onChange` callback is called whenever the editor's content changes. This is the primary way to track modifications to the document.
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/reference/editor/manipulating-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ if (selectedText) {
getActiveStyles(): Styles
```

Returns the active text styles at the current cursor position or at the end of the current selection.
Returns the active text styles at the current cursor position or at the end of the current selection. With an empty selection, this includes styles enabled for upcoming text: for example, turning on bold before typing makes `activeStyles.bold` true even when the surrounding text is not bold.

```typescript
const activeStyles = editor.getActiveStyles();
Expand All @@ -413,7 +413,7 @@ if (activeStyles.textColor) {
getSelectedLinkUrl(): string | undefined
```

Returns the URL of the last link in the current selection, or `undefined` if no links are selected.
Returns the URL of the link the current selection starts in, or `undefined` if it does not start in a link.

```typescript
const linkUrl = editor.getSelectedLinkUrl();
Expand Down
10 changes: 9 additions & 1 deletion docs/content/docs/reference/editor/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,20 @@ To focus the editor, you can use the `focus` method.
editor.focus();
```

Check if the editor has focus.
Check if the editor's content area has focus:

```ts
const isFocused = editor.isFocused();
```

By default, `isFocused()` returns `false` when focus moves into a toolbar, menu, or popover. Pass `includeEditorUI: true` to include the editor's UI:

```ts
const isInteractingWithEditor = editor.isFocused({ includeEditorUI: true });
```

These methods read the current focus state. To respond to focus changes, use [`onFocusChange`](/docs/reference/editor/events#onfocuschange), or [`useEditorFocus`](/docs/react/overview#useeditorfocus) to render React UI based on focus.

## Undo/Redo

To undo the last operation, you can use the `undo` method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {

import { ResetBlockTypeItem } from "./ResetBlockTypeItem";

// To avoid rendering issues, it's good practice to define your custom drag
// To avoid rendering issues, we recommend defining your custom drag
// handle menu in a separate component, instead of inline within the `sideMenu`
// prop of `SideMenuController`.
const CustomDragHandleMenu = () => (
Expand Down
1 change: 1 addition & 0 deletions examples/03-ui-components/20-portal-elements/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ This example renders two editors side-by-side, both wrapped in a small `overflow
**Relevant Docs:**

- [UI Components](/docs/react/components)
- [Mobile Formatting Toolbar](/docs/react/components/formatting-toolbar#mobile-formatting-toolbar)
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content"
/>
<title>Exporting documents to PDF (react-pdf, deprecated)</title>
<script>
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,12 +1397,13 @@ export class BlockNoteEditor<
}

/**
* A callback function that runs whenever the editor's content area gains or
* loses DOM focus.
* A callback function that runs when focus changes. By default, this reports
* when the editor's content area gains or loses DOM focus.
*
* Note that `focused: false` only means the content area itself blurred —
* focus may have moved into the editor's own UI (e.g. a toolbar
* popover's input).
* popover's input). Pass `includeEditorUI: true` to report changes to
* combined content and UI focus, allowing focus handoffs to settle.
*
* @param callback The callback to execute.
* @returns A function to remove the callback.
Expand Down
18 changes: 9 additions & 9 deletions packages/core/src/editor/managers/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export type Unsubscribe = () => void;
*/
export type EditorFocusOptions = {
/**
* When true, the editor's own UI - toolbars, menus and popovers, i.e.
* everything portalled into `editor.portalElement` - counts as focused,
* When true, the editor's own UI - toolbars, menus and popovers inside
* its DOM boundary or registered portal elements - counts as focused,
* answering "is the user still interacting with this editor?" rather than
* "does the content area hold DOM focus?".
*
Expand Down Expand Up @@ -112,9 +112,9 @@ export class EventManager<
* Settled focus-within-UI tracking. Document-level listeners (attached on
* editor mount, detached on unmount — a no-op per focus event is too
* cheap to be worth gating on subscribers) cover the case tiptap events
* can't: focus moving from the editor's own UI (which lives in
* `editor.portalElement`, outside the content area) to somewhere else
* entirely. Blur-side changes are re-checked a frame later because
* can't: focus moving from the editor's own UI outside the content area
* (including registered portal elements) to somewhere else entirely.
* Blur-side changes are re-checked after the current task because
* `document.activeElement` transiently becomes `<body>` during focus
* handoffs (and `relatedTarget` is unreliable on mobile).
*/
Expand Down Expand Up @@ -215,13 +215,13 @@ export class EventManager<
}

/**
* Register a callback that will be called when the editor's content area
* gains or loses DOM focus.
* Register a callback for focus changes. By default, this reports when the
* editor's content area gains or loses DOM focus.
*
* Note that `focused: false` only means the content area itself blurred —
* focus may have moved into the editor's own UI (e.g. a toolbar
* popover's input). Consumers that need to distinguish should check where
* `document.activeElement` ended up.
* popover's input). Pass `includeEditorUI: true` to report changes to
* combined content and UI focus, allowing focus handoffs to settle.
*/
public onFocusChange(
callback: (
Expand Down
2 changes: 1 addition & 1 deletion playground/src/examples.gen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ export const examples = {
slug: "ui-components",
},
readme:
"By default, BlockNote's floating UI elements (formatting toolbar, slash menu, table handles, etc.) mount inside the editor's `bn-container`. The `portalElements` prop on `BlockNoteView` lets you change thatglobally via `default`, or per element by key.\n\nThis example renders two editors side-by-side, both wrapped in a small `overflow: hidden` container. The left editor uses the default the slash menu is clipped by the editor's bounds. The right editor passes `portalElements={{ default: document.body }}` so floating UI escapes the wrapper and renders fully.\n\n```tsx\n<BlockNoteView editor={editor} portalElements={{ default: document.body }} />\n```\n\n**Relevant Docs:**\n\n- [UI Components](/docs/react/components)",
"By default, BlockNote's floating components (formatting toolbar, slash menu, table handles, etc.) mount next to the editor, inside its `bn-container` (or inside whatever you render `BlockNoteViewEditor` into). The `portalElements` prop on `BlockNoteView` lets you change that: globally via `default`, or per component by key. The menus and popovers a floating component opens follow it wherever it mounts.\n\nThis example renders two editors side-by-side, both wrapped in a small `overflow: hidden` container. The left editor uses the default, so the slash menu is clipped by the editor's bounds. The right editor passes `portalElements={{ default: document.body }}` so the floating components escape the wrapper and render fully.\n\n```tsx\n<BlockNoteView editor={editor} portalElements={{ default: document.body }} />\n```\n\n**Relevant Docs:**\n\n- [UI Components](/docs/react/components)\n- [Mobile Formatting Toolbar](/docs/react/components/formatting-toolbar#mobile-formatting-toolbar)",
},
],
},
Expand Down
Loading