Skip to content
WANCRAFT

D.196Navigation & Menus

Selection Toolbar

A Medium-style toolbar that floats above a text selection inside one container, keeps the selection intact and is reachable with Alt+F10.

NewReactCSSTailwindadvanced

Overview

Selection Toolbar wraps a piece of content and shows a small floating toolbar whenever text inside it is selected, the way Medium does. It only subscribes to the document's selectionchange event while that container is in use, and it ignores selections anywhere else on the page. The toolbar is positioned from range.getBoundingClientRect() in the top layer. It flips below the selection when there is no room above and clamps to the viewport, and its arrow keeps pointing at the middle of the selection. Its buttons never steal the selection, because mousedown is prevented. Every action callback receives the selected text and a copy of its range; a wrapRange helper makes bold / highlight / comment style markup safe across paragraphs. Keyboard users select with Shift + arrows, press Alt+F10 to move into the toolbar, move between buttons with the arrow keys and press Escape to close it, which puts focus and the selection back where they were. Copy works out of the box.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/selection-toolbar.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/selection-toolbar.json

Method 02 · Copy and paste

  1. 1. No extra dependencies — React and Tailwind CSS only.
  2. 2. Copy the source from the Code tab into components/ui/selection-toolbar.tsx.
  3. 3. Import it and adjust props.
Dependencies
None
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { SelectionToolbar } from "@/components/ui/selection-toolbar";

export function SelectionToolbarExample() {
  return (
    <SelectionToolbar onAction={(id, { text }) => console.log(id, text)}>
      <p>Select any words in this paragraph to see the toolbar.</p>
    </SelectionToolbar>
  );
}

03Props

PropTypeDefaultDescription
childrenReactNode—Required. The content whose selections show the toolbar (plain text or a contentEditable editor).
actionsSelectionToolbarAction[]DEFAULT_SELECTION_ACTIONS{ id, label, icon?, keepOpen?, separator?, onSelect?(ctx) }[]. onSelect may return a short feedback string.
onAction(id: string, ctx: { text; range; container; close }) => void—Called for every action with the selected text and a copy of its Range.
placement"top" | "bottom""top"Preferred side of the selection; flips when there is no room.
offsetnumber10Gap between the selection and the toolbar in px.
minLengthnumber1Minimum selected characters (after trimming).
showLabelsbooleanfalseText labels next to the icons.
variant"contrast" | "surface""contrast"Inverted or card-like toolbar.
labelstring"Text formatting"Accessible name of the toolbar.
accentstring"#FF7448"Focus ring and feedback colour.
classNamestring—Extra classes for the container.

04Features

  • 01Scoped: selectionchange is only subscribed while the container is in use, and selections elsewhere are ignored
  • 02Positioned from range.getBoundingClientRect() in the top layer, with flip, viewport clamping and an arrow aimed at the middle of the selection
  • 03Buttons never steal the selection (mousedown is prevented); a mouse drag shows the toolbar only on release
  • 04Action callbacks get the text and a copy of the Range; wrapRange() wraps text nodes safely across paragraphs
  • 05Six default actions (bold, italic, link, highlight, comment, copy); copy uses the clipboard and confirms with a check
  • 06Alt+F10 moves into the toolbar, the arrow keys rove between buttons and Escape returns focus and the selection
motion · tweeninput · pointerinput · keyboardinput · clickinput · focus

05Accessibility

  • The toolbar is role="toolbar" with an accessible name and roving tabindex: Arrow Left / Right (and Up / Down), Home and End move between buttons; icon-only buttons have aria-labels.
  • After selecting with Shift + arrows (in an editor or with caret browsing), Alt+F10 moves focus into the toolbar. A polite live region announces "Text toolbar available. Press Alt+F10 to reach it." when it appears.
  • Escape closes only the toolbar (it is consumed so a surrounding dialog stays open) and puts focus and the selection back where they were; the Escape listener exists only while the toolbar is open.
  • Feedback such as "Copied" is shown on the button and announced in the live region.
  • Its listeners are scoped: selectionchange only while the container is in use, and keydown / scroll only while the toolbar is open. Nothing is global by default.
  • With prefers-reduced-motion the toolbar only fades in.

06Limitations

  • Formatting is up to you: the callbacks receive the range, and wrapRange() is a simple helper, not a rich-text engine.
  • On iOS and Android the system selection menu still appears alongside the toolbar.