Selection Toolbar

Floating toolbar for text selection actions and formatting.

Installation

npx shadcn@latest add @iconiq/r-selection-toolbar

File Structure

Usage

"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>
    </>
  );
}

Props

Props
Description

SelectionToolbar

containerRef

Ref 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>

className

Optional class names applied to the portaled toolbar shell.

Type string

items

Optional toolbar actions. Defaults to bold, italic, and underline. Use SelectionToolbarPresets for link, copy, and strikethrough helpers.

Type SelectionToolbarItem[]

children

Optional custom toolbar buttons rendered after the configured items.

Type React.ReactNode

onCommand

Called after a native formatting command succeeds.

Type (command: SelectionToolbarCommand) => void

portalContainer

Portal target for the floating toolbar. Defaults to document.body.

Type HTMLElement | null·Default document.body

offset

Gap in pixels between the selection anchor and the toolbar.

Type number·Default 10

side

Preferred placement relative to the selection. Auto flips above or below based on viewport space.

Type "auto" | "top" | "bottom"·Default "auto"

zIndex

Stacking order for the portaled toolbar.

Type number·Default 50

disabled

When true, the toolbar stays hidden and actions are ignored.

Type boolean·Default false

aria-controls

Optional id of the editable surface controlled by the toolbar.

Type string

Positioning and editing behavior

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.

Registry bundle

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.

Select any phrase here to reveal the toolbar, then try bold, underline, link, or copy on the highlight.