diff --git a/docs/content/docs/getting-started/index.mdx b/docs/content/docs/getting-started/index.mdx
index 8030c5f89a..f6be7f811f 100644
--- a/docs/content/docs/getting-started/index.mdx
+++ b/docs/content/docs/getting-started/index.mdx
@@ -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
```
-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
diff --git a/docs/content/docs/getting-started/vanilla-js.mdx b/docs/content/docs/getting-started/vanilla-js.mdx
index 2ec0e2fbb5..985e00df4b 100644
--- a/docs/content/docs/getting-started/vanilla-js.mdx
+++ b/docs/content/docs/getting-started/vanilla-js.mdx
@@ -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:
diff --git a/docs/content/docs/react/components/formatting-toolbar.mdx b/docs/content/docs/react/components/formatting-toolbar.mdx
index 569e298d6a..ca9cea5fb5 100644
--- a/docs/content/docs/react/components/formatting-toolbar.mdx
+++ b/docs/content/docs/react/components/formatting-toolbar.mdx
@@ -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.
{/* nav, editor, page content... */}
```
-This element must be a direct child of `` 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 ``, 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.
- 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.
+
+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.
diff --git a/docs/content/docs/react/components/index.mdx b/docs/content/docs/react/components/index.mdx
index c068e16ec7..79dd203ddb 100644
--- a/docs/content/docs/react/components/index.mdx
+++ b/docs/content/docs/react/components/index.mdx
@@ -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.
diff --git a/docs/content/docs/react/overview.mdx b/docs/content/docs/react/overview.mdx
index b85d29ab21..b744a65913 100644
--- a/docs/content/docs/react/overview.mdx
+++ b/docs/content/docs/react/overview.mdx
@@ -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.
diff --git a/docs/content/docs/react/styling-theming/overriding-css.mdx b/docs/content/docs/react/styling-theming/overriding-css.mdx
index bece286f4d..c8f3d23549 100644
--- a/docs/content/docs/react/styling-theming/overriding-css.mdx
+++ b/docs/content/docs/react/styling-theming/overriding-css.mdx
@@ -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).
@@ -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:
diff --git a/docs/content/docs/reference/editor/events.mdx b/docs/content/docs/reference/editor/events.mdx
index 137c479b55..facac2a8f4 100644
--- a/docs/content/docs/reference/editor/events.mdx
+++ b/docs/content/docs/reference/editor/events.mdx
@@ -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`
@@ -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.
diff --git a/docs/content/docs/reference/editor/manipulating-content.mdx b/docs/content/docs/reference/editor/manipulating-content.mdx
index 1a9c97c222..27b9bc0e70 100644
--- a/docs/content/docs/reference/editor/manipulating-content.mdx
+++ b/docs/content/docs/reference/editor/manipulating-content.mdx
@@ -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();
@@ -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();
diff --git a/docs/content/docs/reference/editor/overview.mdx b/docs/content/docs/reference/editor/overview.mdx
index 086d91c754..a754ddcd4b 100644
--- a/docs/content/docs/reference/editor/overview.mdx
+++ b/docs/content/docs/reference/editor/overview.mdx
@@ -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.
diff --git a/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx b/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx
index ec50019e0c..d1e073134e 100644
--- a/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx
+++ b/examples/03-ui-components/05-side-menu-drag-handle-items/src/App.tsx
@@ -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 = () => (
diff --git a/examples/03-ui-components/20-portal-elements/README.md b/examples/03-ui-components/20-portal-elements/README.md
index 63e1ba7c55..1c98052d4a 100644
--- a/examples/03-ui-components/20-portal-elements/README.md
+++ b/examples/03-ui-components/20-portal-elements/README.md
@@ -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)
diff --git a/examples/05-interoperability/11-converting-blocks-to-pdf-react-pdf-deprecated/index.html b/examples/05-interoperability/11-converting-blocks-to-pdf-react-pdf-deprecated/index.html
index 4d59fc32bf..875c480942 100644
--- a/examples/05-interoperability/11-converting-blocks-to-pdf-react-pdf-deprecated/index.html
+++ b/examples/05-interoperability/11-converting-blocks-to-pdf-react-pdf-deprecated/index.html
@@ -1,7 +1,10 @@
-
+
Exporting documents to PDF (react-pdf, deprecated)