Mobile Support
How Blokhaus adapts to touch devices.
Blokhaus automatically adapts its UI for touch devices. Desktop-only features like drag handles are disabled, and a mobile-optimized formatting toolbar appears at the bottom of the screen. This happens transparently -- no configuration is needed.
Touch detection
Blokhaus detects touch devices on mount using multiple signals for reliability:
const isTouch =
window.matchMedia("(pointer: coarse)").matches ||
navigator.maxTouchPoints > 0 ||
"ontouchstart" in window;This check runs once on mount and is not reactive. Screen type does not change at runtime in production -- a device is either touch or not. Multiple signals are used because Chrome DevTools device emulation does not always set pointer: coarse.
The detection runs inside a useEffect to avoid SSR hydration mismatches (window is not available on the server).
What changes on mobile
OverlayPortal is disabled
The OverlayPortal component (which renders the drag handle for block reordering) checks for touch capability on mount. If the device is a touch device, the component renders nothing and returns immediately:
// Inside OverlayPortal:
const isTouch = window.matchMedia("(pointer: coarse)").matches;
if (isTouch) return null;No drag handles are rendered, and no mousemove listeners are attached. This avoids unnecessary DOM event overhead on mobile.
MobileToolbar renders at the bottom
The MobileToolbar component renders a fixed-position formatting toolbar at the bottom of the viewport when the user selects text inside the editor. It is completely hidden on non-touch devices.
TableHoverActions are disabled
Table hover actions (the row/column add buttons that appear when hovering over table edges) are also disabled on touch devices, since hover interactions are not available.
MobileToolbar
Setup
Add the MobileToolbar as a child of EditorRoot:
import { EditorRoot, MobileToolbar, FloatingToolbar } from "@blokhaus/core";
export default function EditorPage() {
return (
<EditorRoot
namespace="my-editor"
className="min-h-[400px] p-4 border rounded"
>
<FloatingToolbar />
<MobileToolbar />
</EditorRoot>
);
}You can include both FloatingToolbar and MobileToolbar in the same editor. They will not conflict -- FloatingToolbar is for desktop (shows on text selection via mouse), and MobileToolbar only renders on touch devices.
Features
The MobileToolbar provides five formatting buttons:
| Button | Format | Icon |
|---|---|---|
| Bold | bold | B |
| Italic | italic | I |
| Underline | underline | U |
| Strikethrough | strikethrough | |
| Inline Code | code | <> |
Each button dispatches a FORMAT_TEXT_COMMAND to the editor. The toolbar handles both onMouseDown and onTouchStart events to ensure reliable behavior across different mobile browsers.
Visibility logic
The toolbar uses the selectionchange DOM event to determine when to show or hide:
- Show: When the user selects text (non-collapsed selection) inside the editor's root element.
- Hide: When the selection is collapsed (cursor only), when the selection is outside the editor, or when no selection exists.
document.addEventListener("selectionchange", () => {
const selection = window.getSelection();
// Hide if selection is collapsed or empty
if (!selection || selection.isCollapsed) {
setIsVisible(false);
return;
}
// Hide if selection is outside our editor
if (!rootElement.contains(selection.anchorNode)) {
setIsVisible(false);
return;
}
setIsVisible(true);
});Positioning
The toolbar uses position: fixed with bottom: 0, anchoring it to the bottom of the viewport. It spans the full width of the screen.
For devices with a notch or home indicator (iPhone X and later), the toolbar accounts for the safe area:
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));This ensures the toolbar buttons are not obscured by the device's home indicator.
Styling
The toolbar uses the same frosted glass aesthetic as other Blokhaus popovers:
backdrop-filter: blur(20px) saturate(180%)- Background:
var(--blokhaus-popover-bg) - Border:
var(--blokhaus-popover-border)(top border only) - Button size: 44x44px (Apple's recommended minimum touch target)
- z-index: 50
Props
MobileToolbar takes no props. It reads the editor instance from the LexicalComposer context automatically.
Block reordering on mobile
Block reordering via drag-and-drop is not supported on mobile in v1. The drag handle (OverlayPortal) is disabled entirely on touch devices.
This is a deliberate design decision. Drag-and-drop on mobile requires complex gesture handling (long-press to initiate, scroll-while-dragging, visual feedback) that is beyond the scope of the initial release.
If you need block reordering on mobile, consider implementing a custom solution using:
- A long-press gesture to enter "reorder mode"
- Up/Down buttons on each block
- The
node.insertBefore()andnode.insertAfter()Lexical APIs for actual node movement
Testing mobile behavior
Use Playwright's device emulation to test mobile-specific behavior:
import { test, devices } from "@playwright/test";
test.use({ ...devices["iPhone 15 Pro"] });
test("mobile toolbar appears on text selection", async ({ page }) => {
await page.goto("/editor");
// Type some text
const editor = page.locator("[data-blokhaus-root]");
await editor.click();
await page.keyboard.type("Hello World");
// Select text
await page.keyboard.down("Shift");
for (let i = 0; i < 5; i++) {
await page.keyboard.press("ArrowLeft");
}
await page.keyboard.up("Shift");
// Mobile toolbar should be visible
await expect(page.locator('[data-testid="mobile-toolbar"]')).toBeVisible();
});
test("drag handle is not rendered on mobile", async ({ page }) => {
await page.goto("/editor");
const editor = page.locator("[data-blokhaus-root]");
await editor.click();
await page.keyboard.type("Some text");
// Drag handle should not exist
await expect(page.locator('[data-testid="drag-handle"]')).not.toBeVisible();
});Related
- Multi-Editor -- Multiple editors on one page
- API: MobileToolbar -- Full API reference
- API: OverlayPortal -- Desktop drag handle reference