Definition
This is a pattern about how to decide when to show a floating toolbar (a small popup with buttons like bold·italic) that hovers over text when the user drag-selects it.
The common approach is to hide the toolbar on mouse press (mousedown) and show it again on release (mouseup), but this article argues for updating it instead via the document's selectionchange event (an event that fires whenever the selection range changes in the browser). Because it recomputes the bundle of info the toolbar needs (a snapshot — collecting at once the state such as whether the selection is empty, its on-screen rect, whether it's bold) each time the selection range changes, you can show the toolbar even mid-drag.
Why it's needed
Hiding on mouse press and showing on release produces an experience where "the toolbar only appears after the selection is fully finished." If you want to show a highlight live during the drag or bring up format buttons right away, you have to sync to the change of the selection itself, not the mouse state.
Moreover, relying only on mouse events breaks easily: HTML5 drag and drop (the browser's built-in feature for dragging and dropping elements) sometimes doesn't send mouseup after a drop finishes, so the toolbar-display logic can get stuck in a "pressed but no release signal received" state. selectionchange avoids this pitfall.
How it works
- The user drags text with a pointer.
- The browser updates the Selection range and fires
selectionchange. - The handler reads
window.getSelection()·the anchor/focus node and computes the toolbar snapshot (empty, rect, marks). - If not empty, it renders the floating UI.
- In a hybrid editor, if the anchor/focus is outside the "current guest container," the single-block toolbar hides and the higher range-level toolbar owns it — preventing dual UI.
selectionchangedoes not fire on scroll — anchor coordinate drift is handled separately with a scroll listener.
Practical application
- Remove the
isSelectingText+ mousedown/mouseup +requestAnimationFrameband-aid. - The single-block guest keeps bold/italic via editor transactions; only the cross-boundary case uses a document-level handler + patch.
- Test: after
document.execCommand('selectAll')or a programmatic selection, verify the toolbar appears without a mouseup. - Visually verify drag selection·cross-boundary selection·no double toolbar in Storybook·the browser.
Trade-offs
selectionchangecan fire frequently during input — keep snapshot building light, and bail on rect equality.- With shadow DOM·multiple contenteditables, you need container-boundary detection.
- Unifying everything into the patch path risks regressing existing align·PM semantics — keeping the single block as a guest is often safer.
When not to use
- A fixed toolbar unrelated to selection (always visible).
- A UI with only block reorder DnD handles — there's no text selection, so a format toolbar is meaningless.
- "Always visible" is required but the selection is actually empty — keep the
!emptyself-gate.
Common mistakes
- Binding block DnD and text-selection UI to the same mouse flag.
- Showing the single·range toolbars simultaneously across a boundary (missing container guard).
- Trying to solve scroll anchoring with selectionchange alone.
- No layout rect in jsdom → not deferring visual verification to the browser.
Related concepts
- fixed-position-floating-ui-scroll — re-measuring the anchor on scroll
- focused-guest-editor — leasing one guest block
- contenteditable-keyboard-history-delegation — keyboard·history delegation