RTEBubble Menu
- Stable
- WCAG 2.2 evidence
- RTL
A floating formatting toolbar next to selected text. A non-modal React Aria Popover
anchored to triggerRef (or to a rectangle from getTargetRect) holding a role="toolbar": the editor stays usable
while it is shown, nothing is trapped (WCAG 2.1.2), the tools are one tab stop with arrows, Home and End (2.1.1), and
Escape inside the menu closes it and returns focus to where it was before (the editor) (2.4.3). The menu flips to stay
inside the viewport and wraps in narrow ones (1.4.10). It supplements the main toolbar and is never the only way to format. Consumer duties: open it
on selection (isOpen), close it when the selection collapses, give it a name, and offer a keyboard way in (shouldFocusOnOpen
with a shortcut such as Alt+F10).
import { RTEBubbleMenu } from "@nexera-ui/react";- 3
- examples
- 23
- props
- 3
- live controls
- 1
- platform
- 7
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { RTEBubbleMenu } from "@nexera-ui/react";
<RTEBubbleMenu />Examples 2
The same examples as Storybook, rendered live. Open Code to copy one.
On selection
Realistic use with a plain contenteditable element: select text to show the menu above the selection (getTargetRect); press Alt+F10 in the editor to move focus into it; Escape returns to the editor.
import { useEffect, useRef, useState } from "react";
import { RTEBubbleMenu, RTETool, RTEToolSelect, RTEToolbarDivider } from "@nexera-ui/react";
import {
LuBold,
LuCode,
LuHighlighter,
LuItalic,
LuLink,
LuStrikethrough,
LuUnderline,
} from "react-icons/lu";
const BLOCKS = [
{ id: "paragraph", label: "Paragraph" },
{ id: "h2", label: "Heading 2" },
{ id: "h3", label: "Heading 3" },
];
function Tools() {
return (
<>
<RTEToolSelect aria-label="Block type" options={BLOCKS} defaultValue="paragraph" />
<RTEToolbarDivider />
<RTETool
aria-label="Bold"
icon={<LuBold />}
defaultSelected
shortcut="Ctrl+B"
aria-keyshortcuts="Control+B"
/>
<RTETool
aria-label="Italic"
icon={<LuItalic />}
shortcut="Ctrl+I"
aria-keyshortcuts="Control+I"
/>
<RTETool
aria-label="Underline"
icon={<LuUnderline />}
shortcut="Ctrl+U"
aria-keyshortcuts="Control+U"
/>
<RTETool aria-label="Strikethrough" icon={<LuStrikethrough />} />
<RTETool aria-label="Inline code" icon={<LuCode />} />
<RTETool aria-label="Highlight" icon={<LuHighlighter />} />
<RTEToolbarDivider />
<RTETool
kind="action"
aria-label="Link"
icon={<LuLink />}
shortcut="Ctrl+K"
aria-keyshortcuts="Control+K"
/>
</>
);
}
export function OnSelection() {
const editor = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const [focusOnOpen, setFocusOnOpen] = useState(false);
useEffect(() => {
const onSelectionChange = () => {
const selection = document.getSelection();
const inside =
selection !== null &&
!selection.isCollapsed &&
editor.current?.contains(selection.anchorNode) === true;
if (inside) {
setFocusOnOpen(false);
setOpen(true);
}
};
document.addEventListener("selectionchange", onSelectionChange);
return () => {
document.removeEventListener("selectionchange", onSelectionChange);
};
}, []);
return (
<div className="mt-20 flex max-w-prose flex-col gap-2">
<span id="bubble-editor-label" className="text-component-label text-primary">
Announcement
</span>
<div
ref={editor}
role="textbox"
aria-multiline="true"
aria-labelledby="bubble-editor-label"
aria-keyshortcuts="Alt+F10"
contentEditable
suppressContentEditableWarning
tabIndex={0}
className="min-h-24 rounded-md border border-solid border-input-border bg-input p-4 text-body-large text-primary outline-hidden focus-visible:focus-ring"
onKeyDown={(event) => {
if (event.altKey && event.key === "F10") {
event.preventDefault();
setFocusOnOpen(true);
setOpen(true);
}
}}
>
Open enrollment starts on 1 November. Select some of this text to format it.
</div>
<RTEBubbleMenu
aria-label="Text formatting"
triggerRef={editor}
getTargetRect={() => {
const selection = document.getSelection();
return selection && selection.rangeCount > 0
? selection.getRangeAt(0).getBoundingClientRect()
: null;
}}
isOpen={open}
onOpenChange={setOpen}
shouldFocusOnOpen={focusOnOpen}
>
<Tools />
</RTEBubbleMenu>
</div>
);
}
In editor
A realistic composed editor with a plain contenteditable element (select text in the editor to show the bubble menu; Alt+F10 moves into it).
export function InEditor() {
return <RichTextEditorDemo />;
}
Props 23
Press "Try it" on a card to load that prop into the playground.
23 props shown
children*NexeraReactNodeThe tools. They behave like an RTEToolbar: one tab stop, arrows, Home and End.
triggerRef*NexeraRefObject<Element | null>The element the menu is positioned against, usually the editor's content element. With getTargetRect the menu is
placed against a rectangle inside it instead, for example the selection
(() => window.getSelection()?.getRangeAt(0).getBoundingClientRect()). The library has no selection engine.
shouldFocusOnOpenNexerabooleanMoves focus to the first tool when the menu opens. Use it when the menu is opened from the keyboard (for example Alt+F10); leave it off when it appears on text selection, so typing continues in the editor.
placementNexera"bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom"Side of the anchor the menu opens on; it flips to stay inside the viewport.
classNameNexerastringExtra classes for the surface, merged last so they win over the defaults.
styleNexeraCSSPropertiesInline style for the surface.
aria-labelNexerastringAccessible name of the toolbar ("Text formatting"). Translate it.
Accessible name; aria-labelledby wins when both are set.
aria-labelledbyNexerastringId(s) of element(s) that name the toolbar; wins over aria-label.
Id(s) of the visible element(s) that name the toolbar.
isOpenReact AriabooleanWhether the overlay is open by default (controlled).
containerPaddingReact ArianumberThe placement padding that should be applied between the element and its surrounding container.
offsetReact ArianumberThe additional offset applied along the main axis between the element and its anchor element.
crossOffsetReact ArianumberThe additional offset applied along the cross axis between the element and its anchor element.
shouldFlipReact AriabooleanWhether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely.
boundaryElementReact AriaElementElement that that serves as the positioning boundary.
scrollRefReact AriaRefObject<Element | null>A ref for the scrollable region within the overlay.
shouldUpdatePositionReact AriabooleanWhether the overlay should update its position automatically.
maxHeightReact ArianumberThe maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height.
arrowBoundaryOffsetReact ArianumberThe minimum distance the arrow's edge should be from the edge of the overlay element.
getTargetRectReact Aria(target: Element) => DOMRect | nullOverrides the target element's bounding rectangle. Useful for positioning relative to a specific point such as the mouse cursor (e.g. context menus) or text selection. @param target - The target element.
isKeyboardDismissDisabledReact AriabooleanWhether pressing the escape key to close the popover should be disabled. Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided.
shouldCloseOnInteractOutsideReact Aria(element: Element) => booleanWhen user interacts with the argument element outside of the popover ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the popover. By default, onClose will always be called on interaction outside the popover ref.
defaultOpenReact AriabooleanWhether the overlay is open by default (uncontrolled).
onOpenChangeReact Aria(isOpen: boolean) => voidHandler that is called when the overlay's open state changes.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
4 direct · 3 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.3.2Meaningful SequenceLevel A · supporting test
- 1.4.10ReflowLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.3.3Animation from InteractionsLevel AAA · supporting test
- 2.4.3Focus OrderLevel A · tested directly
- 4.1.2Name, Role, ValueLevel A · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<RTEBubbleMenu className="shadow-sm" />Used in blocks
Related components
- CalendarDayOne day of a {@link CalendarMonth } grid, built on React Aria `CalendarCell`: a `gridcell` whose button is named by the full, localised date ("Wednesday, October 14, 2026"), with `aria-selected`, `aria-disabled` and the "today" and range descriptions read by screen readers (WCAG 1.3.1, 4.1.2).
- CalendarMonthA month calendar for picking a date or a date range: header with previous / next buttons and the localised month name, weekday row, and six weeks of `CalendarDay`s.
- CheckboxThe bare 18 px checkbox: one independent yes/no choice applied on submit, or a row selector in a table, list or tree.
- CheckboxCardA large checkbox option with an icon, a title and a description, for a few options that need explanation (notification channels, benefits).
- CheckboxFieldA checkbox with a label and optional helper text: the box (the same element as `Checkbox`) followed by the label and description, applied on submit.
- ColorInputA hex colour field with a swatch and a picker popover.
- ColorPickerPicks a colour.
- ColorTokenCardDocumentation card for one colour token: a 96 px colour sample with an "Aa" text sample, then the name, token path, hex value and contrast note as text.