Floating toolbar for text selection actions and formatting.
npx shadcn@latest add @iconiq/r-selection-toolbar"use client";
import type {
ClipboardEvent,
DragEvent,
FormEvent,
KeyboardEvent,
} from "react";
import { useRef } from "react";
import { SelectionToolbar, SelectionToolbarPresets } from "@/components/ui/r-selection-toolbar";
const previewText = "Select any phrase here to reveal the toolbar, then try bold, underline, link, or copy on the highlight.";
function isBlockedMutationInput(inputType: string) {
return (
inputType.startsWith("insert") ||
inputType.startsWith("delete") ||
inputType.startsWith("history")
);
}
export function SelectionToolbarPreview() {
const editorRef = useRef<HTMLDivElement | null>(null);
const handleBeforeInput = (event: FormEvent<HTMLDivElement>) => {
const inputType = (event.nativeEvent as InputEvent).inputType ?? "";
if (isBlockedMutationInput(inputType)) {
event.preventDefault();
}
};
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (
event.key === "Backspace" ||
event.key === "Delete" ||
event.key === "Enter" ||
(event.key.length === 1 &&
!event.altKey &&
!event.ctrlKey &&
!event.metaKey)
) {
event.preventDefault();
}
};
const handleClipboard = (event: ClipboardEvent<HTMLDivElement>) => {
event.preventDefault();
};
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
};
return (
<>
<SelectionToolbar
containerRef={editorRef}
items={[
{ command: "bold", id: "bold", label: "Bold" },
{ command: "italic", id: "italic", label: "Italic" },
{ command: "underline", id: "underline", label: "Underline" },
SelectionToolbarPresets.strikeThrough,
SelectionToolbarPresets.link,
SelectionToolbarPresets.copy,
]}
/>
<div className="mx-auto w-full max-w-xl text-center">
<div
aria-label="Editable preview"
className="text-balance text-[15px] text-foreground leading-7 outline-none sm:text-[16px]"
contentEditable
id="selection-toolbar-editor"
onBeforeInput={handleBeforeInput}
onCut={handleClipboard}
onDrop={handleDrop}
onKeyDown={handleKeyDown}
onPaste={handleClipboard}
ref={editorRef}
spellCheck={false}
suppressContentEditableWarning
>
{previewText}
</div>
</div>
</>
);
}containerRefRef pointing at the editable container whose text selection should drive the toolbar. Selections outside this element immediately hide the toolbar.
Type React.RefObject<HTMLElement | null>
classNameOptional class names applied to the portaled toolbar shell.
Type string
itemsOptional toolbar actions. Defaults to bold, italic, and underline. Use SelectionToolbarPresets for link, copy, and strikethrough helpers.
Type SelectionToolbarItem[]
childrenOptional custom toolbar buttons rendered after the configured items.
Type React.ReactNode
onCommandCalled after a native formatting command succeeds.
Type (command: SelectionToolbarCommand) => void
portalContainerPortal target for the floating toolbar. Defaults to document.body.
Type HTMLElement | null·Default document.body
offsetGap in pixels between the selection anchor and the toolbar.
Type number·Default 10
sidePreferred placement relative to the selection. Auto flips above or below based on viewport space.
Type "auto" | "top" | "bottom"·Default "auto"
zIndexStacking order for the portaled toolbar.
Type number·Default 50
disabledWhen true, the toolbar stays hidden and actions are ignored.
Type boolean·Default false
aria-controlsOptional id of the editable surface controlled by the toolbar.
Type string
The toolbar positions itself from the live range rectangle returned by the browser selection API and applies formatting through the native rich-text command pipeline.
Coordinates come from Range.getBoundingClientRect in viewport space and the toolbar uses position: fixed, so it stays anchored while the page scrolls.
Horizontal placement is clamped to the viewport and vertical placement can flip above or below the selection when space is limited.
The toolbar is portaled outside clipping containers by default, which avoids overflow-hidden wrappers trimming the floating shell.
The current implementation depends on document.execCommand for inline formatting. That keeps the install lightweight for native contentEditable text, but apps with a custom text model should replace command handling via items or onCommand.
Install the exact registry entry shown on the right when you want the component file and its declared runtime dependencies together.
Dependencies: @radix-ui/react-toolbar, lucide-react, selectiontoolbar.
Installs the Radix-backed selection toolbar plus the shared core selection controller from selectiontoolbar.
Keeps the same containerRef API, viewport-aware positioning, keyboard shortcuts, and dark toolbar shell as the core component.
The generated registry file is /r/r-selection-toolbar.json.
Contact
Additionally, if you find any bug or issue, feel free to raise an issue.